@blocklet/xss
Version:
blocklet prevent xss attack
148 lines (147 loc) • 5.72 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.sanitizeSvg = exports.isSvgFile = exports.initSanitize = void 0;
var xss = _interopRequireWildcard(require("xss"));
var _omit = _interopRequireDefault(require("lodash/omit"));
var _path = _interopRequireDefault(require("path"));
function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; }
function _getRequireWildcardCache(e) { if ("function" != typeof WeakMap) return null; var r = new WeakMap(), t = new WeakMap(); return (_getRequireWildcardCache = function (e) { return e ? t : r; })(e); }
function _interopRequireWildcard(e, r) { if (!r && e && e.__esModule) return e; if (null === e || "object" != typeof e && "function" != typeof e) return { default: e }; var t = _getRequireWildcardCache(r); if (t && t.has(e)) return t.get(e); var n = { __proto__: null }, a = Object.defineProperty && Object.getOwnPropertyDescriptor; for (var u in e) if ("default" !== u && {}.hasOwnProperty.call(e, u)) { var i = a ? Object.getOwnPropertyDescriptor(e, u) : null; i && (i.get || i.set) ? Object.defineProperty(n, u, i) : n[u] = e[u]; } return n.default = e, t && t.set(e, n), n; }
const ignoreTagList = [
// here is a blacklist
"script", "img", "iframe", "body", "form", "style", "link", "meta", "bgsound", "svg", "embed", "object", "video", "audio", "source", "track", "marquee", "blink", "noscript", "param", "textarea", "input", "select", "button"];
const ignoreTagMap = ignoreTagList.reduce((acc, item) => {
acc[item] = true;
return acc;
}, {});
let defaultOptions = {
escapeHtml: str => str,
whiteList: (0, _omit.default)(xss.getDefaultWhiteList(), ignoreTagList),
onIgnoreTag: function (tag, html, options) {
if (ignoreTagMap[tag]) {
return "";
}
},
stripIgnoreTagBody: ["script"]
};
const initSanitize = (_options = {}) => {
const options = {
...defaultOptions,
..._options
};
const xssInstance = new xss.FilterXSS(options);
const sanitize = data => {
if (typeof data === "string") {
return xssInstance.process(data);
}
if (Array.isArray(data)) {
return data.map(item => {
if (typeof item === "string") {
return xssInstance.process(item);
}
if (Array.isArray(item) || typeof item === "object") {
return sanitize(item);
}
return item;
});
}
if (typeof data === "object" && data !== null) {
Object.keys(data).forEach(key => {
if (options?.allowedKeys?.includes(key)) {
return;
}
const item = data[key];
if (typeof item === "string") {
data[key] = xssInstance.process(item);
} else if (Array.isArray(item) || typeof item === "object") {
data[key] = sanitize(item);
}
});
}
return data;
};
return sanitize;
};
exports.initSanitize = initSanitize;
const svgWhiteList = {
svg: ["width", "height", "viewBox", "xmlns", "version", "preserveAspectRatio", "xml:space"],
circle: ["cx", "cy", "r", "fill", "stroke", "stroke-width", "fill-opacity", "stroke-opacity"],
ellipse: ["cx", "cy", "rx", "ry", "fill", "stroke", "stroke-width"],
line: ["x1", "y1", "x2", "y2", "stroke", "stroke-width"],
path: ["d", "fill", "stroke", "stroke-width", "fill-rule", "stroke-linecap", "stroke-linejoin"],
polygon: ["points", "fill", "stroke", "stroke-width"],
polyline: ["points", "fill", "stroke", "stroke-width"],
rect: ["x", "y", "width", "height", "rx", "ry", "fill", "stroke", "stroke-width"],
g: ["transform", "fill", "stroke"],
text: ["x", "y", "font-size", "font-family", "text-anchor", "fill"],
defs: [],
clipPath: ["id"],
mask: ["id"],
use: ["x", "y", "width", "height"],
linearGradient: ["id", "x1", "y1", "x2", "y2", "gradientUnits"],
radialGradient: ["id", "cx", "cy", "r", "fx", "fy", "gradientUnits"],
stop: ["offset", "stop-color", "stop-opacity"],
pattern: ["id", "width", "height", "patternUnits", "patternTransform"]
};
const svgSanitizeOptions = {
whiteList: svgWhiteList,
stripIgnoreTagBody: ["script", "style"],
onIgnoreTag: function (tag, html, options) {
return "";
},
onIgnoreTagAttr: function (tag, name, value, isWhiteAttr) {
if (name.startsWith("on") || name === "href" || name === "xlink:href") {
return "";
}
if (name === "style") {
const safeValue = value.replace(/expression\(.*\)|javascript:|data:|@import|behavior|binding|moz-binding/gi, "");
if (safeValue !== value) {
return "";
}
return `${name}="${safeValue}"`;
}
if (tag === "use" && (name === "href" || name === "xlink:href")) {
if (value.startsWith("#") && !/[<>"']/.test(value)) {
return `${name}="${value}"`;
}
return "";
}
if (name === "id" || name === "class") {
return `${name}="${value}"`;
}
}
};
const sanitizeSvg = svgContent => {
const isSvg = isSvgFile(svgContent);
if (!isSvg) {
throw new Error("Invalid SVG content");
}
const xssInstance = new xss.FilterXSS(svgSanitizeOptions);
return xssInstance.process(svgContent);
};
exports.sanitizeSvg = sanitizeSvg;
const isSvgFile = (svgContent, file) => {
if (typeof svgContent !== "string") {
return false;
}
const svgRegex = /<svg[^>]*?(?:>|\/>)|<\?xml[^>]*>\s*<svg[^>]*?(?:>|\/?>)/i;
const isSvg = svgRegex.test(svgContent);
if (!isSvg) {
return false;
}
if (file?.name) {
const ext = _path.default.extname(file.name).toLowerCase();
if (ext !== ".svg") {
return false;
}
}
if (file?.type) {
if (!file.type.toLowerCase().includes("image/svg")) {
return false;
}
}
return true;
};
exports.isSvgFile = isSvgFile;