UNPKG

@blocklet/xss

Version:

blocklet prevent xss attack

148 lines (147 loc) 5.72 kB
"use strict"; 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;