mirror of
https://github.com/johannesjo/super-productivity.git
synced 2026-07-25 08:53:50 +00:00
fix(plugins): sanitize untrusted plugin svg icons
Harden plugin icon handling by sanitizing uploaded SVG content before persistence, registration, and rendering to prevent scriptable payloads in trusted HTML sinks. Also tighten SVG URL handling to fragment-only references and add tests for sanitizer behavior across plugin flows. Add dependency overrides to mitigate vulnerable esbuild usage in the audited toolchain where possible. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
parent
71a9a4338a
commit
bcd4cd19dc
8 changed files with 669 additions and 14 deletions
484
package-lock.json
generated
484
package-lock.json
generated
|
|
@ -27535,6 +27535,490 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"node_modules/vite/node_modules/@esbuild/aix-ppc64": {
|
||||
"version": "0.28.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.1.tgz",
|
||||
"integrity": "sha512-Svl7tq8k/08+p6CXPpRjQ1fKX+1odH/BQbb48fV6fj3CWHhsoIOoY87w1oHXm0qEpkIK3ZfVgp0hed3XBXzXMQ==",
|
||||
"cpu": [
|
||||
"ppc64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"aix"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/vite/node_modules/@esbuild/android-arm": {
|
||||
"version": "0.28.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.28.1.tgz",
|
||||
"integrity": "sha512-0k2F129Xdio1TdJfzJ8sy1Q47vUD2NnwdhiAf7drUN1EBTfPf4hsFCtmMgu/6m8JSzsBrlmVjudMBQqOfG8usQ==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"android"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/vite/node_modules/@esbuild/android-arm64": {
|
||||
"version": "0.28.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.28.1.tgz",
|
||||
"integrity": "sha512-34EGEbCIAgosYz6goLcopX6Mo7NyGv9tfwEM2/7Ce2VcVRk568iSvniGWcUXIy7wEDR1wzolcxcriFVrWYcwBg==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"android"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/vite/node_modules/@esbuild/android-x64": {
|
||||
"version": "0.28.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.28.1.tgz",
|
||||
"integrity": "sha512-dbwY7ltSMDWsRatcRpCnES4F+im88OCUgGZjy52shC7GqHRE/cYlxNbB4Z4UpJswpcc4Qxd2oE/ufM0p61IKng==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"android"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/vite/node_modules/@esbuild/darwin-arm64": {
|
||||
"version": "0.28.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.28.1.tgz",
|
||||
"integrity": "sha512-TZbWkQY7kvTAXbXUT7uVACR5cMHsDiSz9z7ZKAX/RTq/WJEk3QyRr0wZpNhBDX+/0CtdqUIJlOiodQcta6tY3Q==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/vite/node_modules/@esbuild/darwin-x64": {
|
||||
"version": "0.28.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.28.1.tgz",
|
||||
"integrity": "sha512-zfdzgK9ACBNZLI/CyHTOx81SyNbM6YXn7rxSgX97VjyiPl9W1i4Ka4fgKECEoFCKGpvBj5qArWIGgQjOwkgskQ==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/vite/node_modules/@esbuild/freebsd-arm64": {
|
||||
"version": "0.28.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.1.tgz",
|
||||
"integrity": "sha512-wG2EA8ENdEI0qhkSZMjfqrdY+ziCYCPMmtZjjIwOmXFjmyzEHn+UUxk5of+SYsjtfs3VpnlC7QLzSI5hY/rOAw==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"freebsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/vite/node_modules/@esbuild/freebsd-x64": {
|
||||
"version": "0.28.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.28.1.tgz",
|
||||
"integrity": "sha512-i7dZ9vQgnvSCzi/rYCXNgtF/U+eKZNJBzu3eTQbRgHnM7tNSizLOkRFAl3qzVc/Op/u5YkHHa4pf/3DOYHthLQ==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"freebsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/vite/node_modules/@esbuild/linux-arm": {
|
||||
"version": "0.28.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.28.1.tgz",
|
||||
"integrity": "sha512-qVXBOHQS+d5Y722GwJzJUtOLlX7km3CraOaGormF1pDtPd2C/l1SHRPgjLunLGe51Sh5YYWKMFDyV4SxgMQYTQ==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/vite/node_modules/@esbuild/linux-arm64": {
|
||||
"version": "0.28.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.28.1.tgz",
|
||||
"integrity": "sha512-yHs+0uc8+nvEAfAfxrWQKK5peSNzBc4PegcMO0EJ2hT71uA7vB8Ihg2e77R2P7SG5uYjPbHlLLmve4LLLRCf0g==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/vite/node_modules/@esbuild/linux-ia32": {
|
||||
"version": "0.28.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.28.1.tgz",
|
||||
"integrity": "sha512-d1z4ZuP0ajrfz/FhGT4vv278rX8KnPPJx8i5+AtK7TYbx9Le9F1hyzurZpkEyjkGa9dUGhQow4C1NmeGvqxN2w==",
|
||||
"cpu": [
|
||||
"ia32"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/vite/node_modules/@esbuild/linux-loong64": {
|
||||
"version": "0.28.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.28.1.tgz",
|
||||
"integrity": "sha512-M5sRjUVZrkm1OAPR3dlOYzNmN+loZKGVi1VUQGrwuqLcbR6qeAz+famMhjASeH3YVKvZz+zT1jlh/keC3Rj/lg==",
|
||||
"cpu": [
|
||||
"loong64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/vite/node_modules/@esbuild/linux-mips64el": {
|
||||
"version": "0.28.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.28.1.tgz",
|
||||
"integrity": "sha512-mRObBZeHh2OxcBFPWE/FjylkRgZdYuiTR3vaTozquCGOH14iP9oN4x4Ge81CoIDYQrXmIxpFumJBu5MtZpnQJQ==",
|
||||
"cpu": [
|
||||
"mips64el"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/vite/node_modules/@esbuild/linux-ppc64": {
|
||||
"version": "0.28.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.28.1.tgz",
|
||||
"integrity": "sha512-slScBsMAb3GFDcdrCgLwZtPYRoH2H/youv10QiZyRjmsP48fznoveWytSgCI/R0ZcUgpc0ZhIUEx6LHts8yrfQ==",
|
||||
"cpu": [
|
||||
"ppc64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/vite/node_modules/@esbuild/linux-riscv64": {
|
||||
"version": "0.28.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.28.1.tgz",
|
||||
"integrity": "sha512-kw0owk1o0GFETUJyW0jc0G4Yzs0BHZn0JDZ8JRT088vjJYX777BAs1fDGxAC+q831qOs2DTC96mNsG2opdfyyQ==",
|
||||
"cpu": [
|
||||
"riscv64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/vite/node_modules/@esbuild/linux-s390x": {
|
||||
"version": "0.28.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.28.1.tgz",
|
||||
"integrity": "sha512-/lAIjX8aYFRByhh6L5rYtPEDRqa9de/4V/juOXcta5frjvzXO4/sqEtyytse0g3zZFuWu5cDN0MkLz2qRDD2Ag==",
|
||||
"cpu": [
|
||||
"s390x"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/vite/node_modules/@esbuild/linux-x64": {
|
||||
"version": "0.28.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.28.1.tgz",
|
||||
"integrity": "sha512-u/anNYF2mmVOEDwLtnQ1wOr3EZ9sTNGLWrsYGYwHWzGA3Si84IOkHXlbWTD1NB+9/1lcnweYKO54uhxZydNzfA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/vite/node_modules/@esbuild/netbsd-arm64": {
|
||||
"version": "0.28.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.1.tgz",
|
||||
"integrity": "sha512-oks0DYbLwWMmaakTsCb+zL4E+aHRVLom9IJZOAthMQEPiQmydXHkziYEsGYRx0uNV/IjEKGAV941JzH02pflqw==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"netbsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/vite/node_modules/@esbuild/netbsd-x64": {
|
||||
"version": "0.28.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.28.1.tgz",
|
||||
"integrity": "sha512-aeL6lAnN89Hz43Mlh1G8ARasbuoYvSITDEx0tHh5b7jJnHcssqgjy9Yx430GDpmCa6OyrKoS0aNRjKundRizGg==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"netbsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/vite/node_modules/@esbuild/openbsd-arm64": {
|
||||
"version": "0.28.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.1.tgz",
|
||||
"integrity": "sha512-MEFJe5C3R8pwXdZ5Y21oo6m7ePiS0d9pWucn99O/wvyJZChoIQKrQDxKrGeW8F5+T0okTHesAmDeiHDTIq0V/Q==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"openbsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/vite/node_modules/@esbuild/openbsd-x64": {
|
||||
"version": "0.28.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.28.1.tgz",
|
||||
"integrity": "sha512-i/ZLIOafE0Z8cI/XANJAixoJL/uRAoS2xOA3rb0xN+KK0K177cMAsQYkzHtBrtMXAKuAc7HGgcWiZ/sRC1Nxgw==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"openbsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/vite/node_modules/@esbuild/openharmony-arm64": {
|
||||
"version": "0.28.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.1.tgz",
|
||||
"integrity": "sha512-ge+Z7EXFNt2BO1oAMsVpiQ8EwndV9i1xXerAeTIK7AtPs3bKFXQM7nlRxDSIUIMeueR1CNXxqztLzdNeReKBJg==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"openharmony"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/vite/node_modules/@esbuild/sunos-x64": {
|
||||
"version": "0.28.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.28.1.tgz",
|
||||
"integrity": "sha512-BEjgtECkL3vY+SaSQ6nzVfiALUeFxpawyp8Jmf5PtYhf1Ug40N1h/hxlhts+f1FvSvarEigdxS3BlSMI2PJLcQ==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"sunos"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/vite/node_modules/@esbuild/win32-arm64": {
|
||||
"version": "0.28.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.28.1.tgz",
|
||||
"integrity": "sha512-lCv9eK/H6ZJWbE7bh2nw54CZ9M2nupBxJcTsdk/QQnWkdSjKGuxmmH8/GWrlT1eMmZfn4dGcCjRte397WqfQXA==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/vite/node_modules/@esbuild/win32-ia32": {
|
||||
"version": "0.28.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.28.1.tgz",
|
||||
"integrity": "sha512-zvb/mB2bSCoJOpoCBgYKKpX6YM6mJBlBUVUtVj41DlZJVEB6/0CKlRYxP5wWl1C1ILiCoAU5wZZ4q1P3qeS6Eg==",
|
||||
"cpu": [
|
||||
"ia32"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/vite/node_modules/@esbuild/win32-x64": {
|
||||
"version": "0.28.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.28.1.tgz",
|
||||
"integrity": "sha512-bm4Mowrv+GXMlpWX++EcXw/iLyd1o3+bJkC2DkWXYVvgZCqD/bSj9ctZeAMC3cIxgjRVR2Dufaiu4YPxr5gW1A==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/vite/node_modules/esbuild": {
|
||||
"version": "0.28.1",
|
||||
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.1.tgz",
|
||||
"integrity": "sha512-HrJrvZv5ayxBzPfwphOoNzkzOIIlifzk0KJrGK2c8R4+LKpMtpYLQeUdjnwjWv/LZlkH2laZk+4w78pi99D4Vw==",
|
||||
"dev": true,
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"esbuild": "bin/esbuild"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@esbuild/aix-ppc64": "0.28.1",
|
||||
"@esbuild/android-arm": "0.28.1",
|
||||
"@esbuild/android-arm64": "0.28.1",
|
||||
"@esbuild/android-x64": "0.28.1",
|
||||
"@esbuild/darwin-arm64": "0.28.1",
|
||||
"@esbuild/darwin-x64": "0.28.1",
|
||||
"@esbuild/freebsd-arm64": "0.28.1",
|
||||
"@esbuild/freebsd-x64": "0.28.1",
|
||||
"@esbuild/linux-arm": "0.28.1",
|
||||
"@esbuild/linux-arm64": "0.28.1",
|
||||
"@esbuild/linux-ia32": "0.28.1",
|
||||
"@esbuild/linux-loong64": "0.28.1",
|
||||
"@esbuild/linux-mips64el": "0.28.1",
|
||||
"@esbuild/linux-ppc64": "0.28.1",
|
||||
"@esbuild/linux-riscv64": "0.28.1",
|
||||
"@esbuild/linux-s390x": "0.28.1",
|
||||
"@esbuild/linux-x64": "0.28.1",
|
||||
"@esbuild/netbsd-arm64": "0.28.1",
|
||||
"@esbuild/netbsd-x64": "0.28.1",
|
||||
"@esbuild/openbsd-arm64": "0.28.1",
|
||||
"@esbuild/openbsd-x64": "0.28.1",
|
||||
"@esbuild/openharmony-arm64": "0.28.1",
|
||||
"@esbuild/sunos-x64": "0.28.1",
|
||||
"@esbuild/win32-arm64": "0.28.1",
|
||||
"@esbuild/win32-ia32": "0.28.1",
|
||||
"@esbuild/win32-x64": "0.28.1"
|
||||
}
|
||||
},
|
||||
"node_modules/vite/node_modules/fsevents": {
|
||||
"version": "2.3.3",
|
||||
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
|
||||
|
|
|
|||
|
|
@ -318,6 +318,7 @@
|
|||
"typia": "^12.0.2"
|
||||
},
|
||||
"overrides": {
|
||||
"esbuild": "0.28.1",
|
||||
"app-builder-lib": {
|
||||
"minimatch": "10.2.5"
|
||||
},
|
||||
|
|
@ -336,6 +337,12 @@
|
|||
"node-gyp": {
|
||||
"undici": "6.27.0"
|
||||
},
|
||||
"vite": {
|
||||
"esbuild": "0.28.1"
|
||||
},
|
||||
"tsup": {
|
||||
"esbuild": "0.28.1"
|
||||
},
|
||||
"sockjs": {
|
||||
"uuid": "11.1.1"
|
||||
},
|
||||
|
|
|
|||
|
|
@ -61,6 +61,7 @@ import { LS } from '../persistence/storage-keys.const';
|
|||
import { Log } from '../log';
|
||||
import { LayoutService } from '../../core-ui/layout/layout.service';
|
||||
import { sanitizeIosKeyboardHeight } from './sanitize-ios-keyboard-height.util';
|
||||
import { sanitizeSvgIconContent } from '../../util/sanitize-svg-icon.util';
|
||||
|
||||
interface NavigationBarPlugin {
|
||||
setColor(options: { color: string; style: 'LIGHT' | 'DARK' }): Promise<void>;
|
||||
|
|
@ -385,9 +386,18 @@ export class GlobalThemeService {
|
|||
registerSvgIconFromContent(iconName: string, svgContent: string): void {
|
||||
// Plugin icon is already registered, skip
|
||||
if (this._registeredPluginIcons.has(iconName)) return;
|
||||
const sanitizedSvg = sanitizeSvgIconContent(svgContent);
|
||||
if (
|
||||
!sanitizedSvg ||
|
||||
!/<svg[\s>]/i.test(sanitizedSvg) ||
|
||||
!/<\/svg>/i.test(sanitizedSvg)
|
||||
) {
|
||||
Log.warn(`Skipping invalid SVG icon registration for ${iconName}`);
|
||||
return;
|
||||
}
|
||||
this._matIconRegistry.addSvgIconLiteral(
|
||||
iconName,
|
||||
this._domSanitizer.bypassSecurityTrustHtml(svgContent),
|
||||
this._domSanitizer.bypassSecurityTrustHtml(sanitizedSvg),
|
||||
);
|
||||
this._registeredPluginIcons.add(iconName);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -31,6 +31,11 @@ describe('PluginService', () => {
|
|||
let pluginLoader: jasmine.SpyObj<PluginLoaderService>;
|
||||
let pluginBridge: jasmine.SpyObj<PluginBridgeService>;
|
||||
let pluginRunner: jasmine.SpyObj<PluginRunner>;
|
||||
let globalThemeServiceMock: {
|
||||
darkMode$: BehaviorSubject<'light'>;
|
||||
darkMode: () => 'light';
|
||||
registerSvgIconFromContent: jasmine.Spy;
|
||||
};
|
||||
|
||||
const mockManifest: PluginManifest = {
|
||||
id: 'test-plugin',
|
||||
|
|
@ -89,6 +94,11 @@ describe('PluginService', () => {
|
|||
'pingNodeBridge',
|
||||
'sendMessageToPlugin',
|
||||
]);
|
||||
globalThemeServiceMock = {
|
||||
darkMode$: new BehaviorSubject('light'),
|
||||
darkMode: () => 'light',
|
||||
registerSvgIconFromContent: jasmine.createSpy('registerSvgIconFromContent'),
|
||||
};
|
||||
|
||||
TestBed.configureTestingModule({
|
||||
providers: [
|
||||
|
|
@ -120,10 +130,7 @@ describe('PluginService', () => {
|
|||
},
|
||||
{
|
||||
provide: GlobalThemeService,
|
||||
useValue: {
|
||||
darkMode$: new BehaviorSubject('light'),
|
||||
darkMode: () => 'light',
|
||||
},
|
||||
useValue: globalThemeServiceMock,
|
||||
},
|
||||
{ provide: PluginMetaPersistenceService, useValue: pluginMetaPersistenceService },
|
||||
{
|
||||
|
|
@ -338,6 +345,34 @@ describe('PluginService', () => {
|
|||
expect(pluginBridge.clearNodeExecutionConsent).toHaveBeenCalledOnceWith(pluginId);
|
||||
});
|
||||
|
||||
it('sanitizes uploaded plugin SVG icons before registration', () => {
|
||||
const sanitizeSvg = (
|
||||
service as unknown as {
|
||||
_sanitizePluginIconSvg: (iconContent: string, iconPath: string) => string | null;
|
||||
}
|
||||
)._sanitizePluginIconSvg.bind(service);
|
||||
|
||||
const result = sanitizeSvg(
|
||||
'<svg onload="alert(1)"><circle cx="5" cy="5" r="4"></circle></svg>',
|
||||
'icon.svg',
|
||||
);
|
||||
|
||||
expect(result).toContain('<svg');
|
||||
expect(result).not.toContain('onload');
|
||||
});
|
||||
|
||||
it('rejects uploaded plugin icon content that is not SVG', () => {
|
||||
const sanitizeSvg = (
|
||||
service as unknown as {
|
||||
_sanitizePluginIconSvg: (iconContent: string, iconPath: string) => string | null;
|
||||
}
|
||||
)._sanitizePluginIconSvg.bind(service);
|
||||
|
||||
const result = sanitizeSvg('<div>not svg</div>', 'icon.svg');
|
||||
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
|
||||
it('clearNodeExecutionConsent returns false when the persisted clear fails (caller can fail closed)', async () => {
|
||||
// A persistence failure must NOT throw (lifecycle bookkeeping ignores it), but must be
|
||||
// reported via the return value so loadPluginFromZip can abort before loading new code
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
/* eslint-disable max-len */
|
||||
import { inject, Injectable, signal, OnDestroy } from '@angular/core';
|
||||
import { inject, Injectable, OnDestroy, signal } from '@angular/core';
|
||||
import { BehaviorSubject, Observable, of } from 'rxjs';
|
||||
import { HttpClient, HttpErrorResponse } from '@angular/common/http';
|
||||
import { environment } from '../../environments/environment';
|
||||
|
|
@ -37,6 +37,7 @@ import { IssueSyncAdapterRegistryService } from '../features/issue/two-way-sync/
|
|||
import { SnackService } from '../core/snack/snack.service';
|
||||
import { pingWithRetry } from './util/ping-with-retry.util';
|
||||
import { PluginBridgeService } from './plugin-bridge.service';
|
||||
import { sanitizeSvgIconContent } from '../util/sanitize-svg-icon.util';
|
||||
|
||||
// Each plugin's `id` (from its manifest.json, distinct from the asset path
|
||||
// here) becomes the entityId prefix for all data it persists via
|
||||
|
|
@ -461,8 +462,24 @@ export class PluginService implements OnDestroy {
|
|||
}
|
||||
|
||||
private _registerPluginIcon(pluginId: string, iconContent: string): void {
|
||||
const sanitizedIcon = this._sanitizePluginIconSvg(
|
||||
iconContent,
|
||||
`${pluginId}/icon.svg`,
|
||||
);
|
||||
if (!sanitizedIcon) {
|
||||
return;
|
||||
}
|
||||
const iconName = `plugin-${pluginId}-icon`;
|
||||
this._globalThemeService.registerSvgIconFromContent(iconName, iconContent);
|
||||
this._globalThemeService.registerSvgIconFromContent(iconName, sanitizedIcon);
|
||||
}
|
||||
|
||||
private _sanitizePluginIconSvg(iconContent: string, iconPath: string): string | null {
|
||||
const sanitizedIcon = sanitizeSvgIconContent(iconContent);
|
||||
if (!sanitizedIcon) {
|
||||
PluginLog.err(`Plugin icon ${iconPath} does not appear to be a valid SVG`);
|
||||
return null;
|
||||
}
|
||||
return sanitizedIcon;
|
||||
}
|
||||
|
||||
private _setPluginState(pluginId: string, state: PluginState): void {
|
||||
|
|
@ -1372,12 +1389,10 @@ export class PluginService implements OnDestroy {
|
|||
}),
|
||||
);
|
||||
}
|
||||
iconContent = new TextDecoder().decode(iconBytes);
|
||||
// Basic SVG validation
|
||||
if (!iconContent.includes('<svg') || !iconContent.includes('</svg>')) {
|
||||
PluginLog.err(`Plugin icon ${manifest.icon} does not appear to be a valid SVG`);
|
||||
iconContent = null;
|
||||
}
|
||||
iconContent = this._sanitizePluginIconSvg(
|
||||
new TextDecoder().decode(iconBytes),
|
||||
manifest.icon,
|
||||
);
|
||||
}
|
||||
|
||||
// Extract config schema if specified in manifest
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ import {
|
|||
import { DomSanitizer } from '@angular/platform-browser';
|
||||
import { PluginService } from '../../plugin.service';
|
||||
import { MatIcon } from '@angular/material/icon';
|
||||
import { sanitizeSvgIconContent } from '../../../util/sanitize-svg-icon.util';
|
||||
|
||||
@Component({
|
||||
selector: 'plugin-icon',
|
||||
|
|
@ -69,6 +70,7 @@ export class PluginIconComponent {
|
|||
return null;
|
||||
}
|
||||
|
||||
return this._sanitizer.bypassSecurityTrustHtml(iconContent);
|
||||
const sanitizedSvg = sanitizeSvgIconContent(iconContent);
|
||||
return sanitizedSvg ? this._sanitizer.bypassSecurityTrustHtml(sanitizedSvg) : null;
|
||||
});
|
||||
}
|
||||
|
|
|
|||
46
src/app/util/sanitize-svg-icon.util.spec.ts
Normal file
46
src/app/util/sanitize-svg-icon.util.spec.ts
Normal file
|
|
@ -0,0 +1,46 @@
|
|||
import { sanitizeSvgIconContent } from './sanitize-svg-icon.util';
|
||||
|
||||
describe('sanitizeSvgIconContent', () => {
|
||||
it('removes scriptable event handlers', () => {
|
||||
const result = sanitizeSvgIconContent(
|
||||
'<svg onload="alert(1)"><circle cx="5" cy="5" r="4"></circle></svg>',
|
||||
);
|
||||
|
||||
expect(result).toContain('<svg');
|
||||
expect(result).not.toContain('onload');
|
||||
});
|
||||
|
||||
it('removes dangerous script and foreignObject nodes', () => {
|
||||
const result = sanitizeSvgIconContent(
|
||||
'<svg><script>alert(1)</script><foreignObject><div>x</div></foreignObject><circle /></svg>',
|
||||
);
|
||||
|
||||
expect(result).toContain('<svg');
|
||||
expect(result).toContain('<circle');
|
||||
expect(result).not.toContain('<script');
|
||||
expect(result).not.toContain('foreignObject');
|
||||
});
|
||||
|
||||
it('removes javascript URLs from links', () => {
|
||||
const result = sanitizeSvgIconContent(
|
||||
'<svg><a href="javascript:alert(1)"><circle /></a></svg>',
|
||||
);
|
||||
|
||||
expect(result).toContain('<svg');
|
||||
expect(result).not.toContain('javascript:');
|
||||
});
|
||||
|
||||
it('removes non-fragment href values', () => {
|
||||
const result = sanitizeSvgIconContent(
|
||||
'<svg><use href="https://example.com/icon.svg#x"></use><use href="#local"></use></svg>',
|
||||
);
|
||||
|
||||
expect(result).toContain('<svg');
|
||||
expect(result).not.toContain('https://example.com');
|
||||
expect(result).toContain('href="#local"');
|
||||
});
|
||||
|
||||
it('returns null for non-svg input', () => {
|
||||
expect(sanitizeSvgIconContent('<div>not svg</div>')).toBeNull();
|
||||
});
|
||||
});
|
||||
56
src/app/util/sanitize-svg-icon.util.ts
Normal file
56
src/app/util/sanitize-svg-icon.util.ts
Normal file
|
|
@ -0,0 +1,56 @@
|
|||
const DANGEROUS_SVG_TAGS = [
|
||||
'script',
|
||||
'foreignObject',
|
||||
'iframe',
|
||||
'object',
|
||||
'embed',
|
||||
'link',
|
||||
'meta',
|
||||
'style',
|
||||
];
|
||||
|
||||
const URI_ATTRS = new Set(['href', 'xlink:href', 'src']);
|
||||
const isSafeSvgReference = (attrValue: string): boolean => attrValue.startsWith('#');
|
||||
|
||||
export const sanitizeSvgIconContent = (svgContent: string): string | null => {
|
||||
if (!svgContent.trim()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const parser = new DOMParser();
|
||||
const doc = parser.parseFromString(svgContent, 'image/svg+xml');
|
||||
if (doc.querySelector('parsererror')) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const root = doc.documentElement;
|
||||
if (!root || root.tagName.toLowerCase() !== 'svg') {
|
||||
return null;
|
||||
}
|
||||
|
||||
for (const tagName of DANGEROUS_SVG_TAGS) {
|
||||
for (const el of Array.from(doc.querySelectorAll(tagName))) {
|
||||
el.remove();
|
||||
}
|
||||
}
|
||||
|
||||
for (const el of Array.from(doc.querySelectorAll('*'))) {
|
||||
for (const attr of Array.from(el.attributes)) {
|
||||
const attrName = attr.name.toLowerCase();
|
||||
const attrValue = attr.value.trim().toLowerCase();
|
||||
if (attrName.startsWith('on') || attrName === 'style') {
|
||||
el.removeAttribute(attr.name);
|
||||
continue;
|
||||
}
|
||||
if (URI_ATTRS.has(attrName) && !isSafeSvgReference(attrValue)) {
|
||||
el.removeAttribute(attr.name);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const sanitized = new XMLSerializer().serializeToString(root);
|
||||
if (!/<svg[\s>]/i.test(sanitized) || !/<\/svg>/i.test(sanitized)) {
|
||||
return null;
|
||||
}
|
||||
return sanitized;
|
||||
};
|
||||
Loading…
Add table
Add a link
Reference in a new issue