Skip to content

Commit abb08c6

Browse files
authored
fix: Security removal dependency svg-prep (parse-community#3236)
1 parent 77c0640 commit abb08c6

3 files changed

Lines changed: 51 additions & 57 deletions

File tree

package-lock.json

Lines changed: 0 additions & 51 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -122,7 +122,6 @@
122122
"semantic-release": "25.0.3",
123123
"semver": "7.7.4",
124124
"style-loader": "3.3.1",
125-
"svg-prep": "1.0.4",
126125
"typescript": "5.9.3",
127126
"webpack": "5.105.1",
128127
"webpack-cli": "6.0.1",

webpack/plugins/svg-prep.js

Lines changed: 51 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,57 @@
99

1010
const fs = require('fs');
1111
const path = require('path');
12-
const SvgPrep = require('svg-prep');
1312
const { Compilation, sources } = require('webpack');
1413
const { RawSource } = sources;
1514

15+
/**
16+
* Builds an SVG sprite from individual SVG files. Each SVG becomes a
17+
* <symbol> element identified by its filename (without extension).
18+
*/
19+
function buildSvgSprite(files) {
20+
const symbols = files.map(file => {
21+
const name = path.basename(file, '.svg');
22+
const svg = fs.readFileSync(file, 'utf-8');
23+
24+
// Extract viewBox from the root <svg> element
25+
const viewBoxMatch = svg.match(/viewBox="([^"]*)"/);
26+
const viewBox = viewBoxMatch ? viewBoxMatch[1] : '0 0 100 100';
27+
28+
// Extract inner content between <svg ...> and </svg>
29+
const innerMatch = svg.match(/<svg[^>]*>([\s\S]*)<\/svg>/);
30+
const inner = innerMatch ? innerMatch[1] : '';
31+
32+
// Strip elements that are unnecessary or potential XSS vectors
33+
// Remove attributes that interfere with sprite styling or pose security risks
34+
const cleaned = inner
35+
.replace(/<style[\s\S]*?<\/style>/gi, '')
36+
.replace(/<defs[\s\S]*?<\/defs>/gi, '')
37+
.replace(/<title[\s\S]*?<\/title>/gi, '')
38+
.replace(/<desc[\s\S]*?<\/desc>/gi, '')
39+
.replace(/<script[\s\S]*?<\/script>/gi, '')
40+
.replace(/<!--[\s\S]*?-->/g, '')
41+
.replace(/\s+id="[^"]*"/g, '')
42+
.replace(/\s+fill="[^"]*"/g, '')
43+
.replace(/\s+class="[^"]*"/g, '')
44+
.replace(/\s+style="[^"]*"/g, '')
45+
.replace(/\s+stroke="[^"]*"/g, '')
46+
.replace(/\s+stroke-[a-z]+="[^"]*"/g, '')
47+
.replace(/\s+on[a-zA-Z]+="[^"]*"/g, '')
48+
.replace(/\s+on[a-zA-Z]+='[^']*'/g, '')
49+
.replace(/\s+href="[^"]*"/g, '')
50+
.replace(/\s+xlink:href="[^"]*"/g, '');
51+
52+
return ` <symbol id="${name}" viewBox="${viewBox}">\n ${cleaned.trim()}\n </symbol>`;
53+
});
54+
55+
return [
56+
'<?xml version="1.0" encoding="UTF-8" standalone="yes"?>',
57+
'<svg id="sprites" xmlns="http://www.w3.org/2000/svg" style="display:none">',
58+
symbols.join('\n'),
59+
'</svg>',
60+
].join('\n');
61+
}
62+
1663
function SvgPrepPlugin(options) {
1764
this.options = {};
1865
Object.assign(
@@ -33,17 +80,16 @@ SvgPrepPlugin.prototype.apply = function (compiler) {
3380
},
3481
async () => {
3582
if (!this.options.source) {
36-
return Promise.resolve();
83+
return;
3784
}
3885

39-
// TODO: Keep track of file hashes, so we can avoid recompiling when none have changed
4086
const files = fs
4187
.readdirSync(this.options.source)
4288
.filter(name => name.endsWith('.svg'))
89+
.sort()
4390
.map(name => path.join(this.options.source, name));
4491

45-
const sprited = await SvgPrep(files).filter({ removeIds: true, noFill: true }).output();
46-
92+
const sprited = buildSvgSprite(files);
4793
compilation.emitAsset(this.options.output, new RawSource(sprited));
4894
}
4995
);

0 commit comments

Comments
 (0)