UNPKG

@blocklet/xss

Version:

blocklet prevent xss attack

197 lines (196 loc) 7.4 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.svgWhiteList = exports.sanitizeSvg = exports.isSvgFile = exports.initSanitize = void 0; var _xss = _interopRequireWildcard(require("xss")); var xss = _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 ""; } }, onTagAttr: (tag, html) => { if (tag === "!BACKUP") { return html; } }, stripIgnoreTagBody: ["script"] }; function advanceProcess(html, options) { while (html !== (0, _xss.filterXSS)(html, options)) { html = (0, _xss.filterXSS)(html, options); } return html; } const initSanitize = (_options = {}) => { const options = { ...defaultOptions, ..._options }; const sanitize = data => { if (typeof data === "string") { return advanceProcess(data, options); } if (Array.isArray(data)) { return data.map(item => { if (typeof item === "string") { return advanceProcess(item, options); } 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] = advanceProcess(item, options); } else if (Array.isArray(item) || typeof item === "object") { data[key] = sanitize(item); } }); } return data; }; return sanitize; }; exports.initSanitize = initSanitize; function preserveTagCase(content) { const tagCaseMapping = {}; Object.keys(svgWhiteList).forEach(originalTag => { const lowerCaseTag = originalTag.toLowerCase(); if (lowerCaseTag !== originalTag) { tagCaseMapping[lowerCaseTag] = originalTag; } }); let result = content; Object.entries(tagCaseMapping).forEach(([lowercase, original]) => { result = result.replace(new RegExp(`<${lowercase}([^>]*)>`, "gi"), (_match, rest) => `<${original}${rest}>`); result = result.replace(new RegExp(`</${lowercase}>`, "gi"), `</${original}>`); }); return result; } function preserveAttrCase(tag, name, value, isWhiteAttr) { if (isWhiteAttr) { const originalTagKey = Object.keys(svgWhiteList).find(key => key.toLowerCase() === tag.toLowerCase()); if (originalTagKey) { const originalAttrName = svgWhiteList[originalTagKey].find(attr => attr.toLowerCase() === name.toLowerCase()); if (originalAttrName) { value = xss.safeAttrValue(tag, name, value); if (value) { return originalAttrName + '="' + value + '"'; } else { return originalAttrName; } } } } } const svgWhiteList = exports.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, options, svgOptions) => { const isSvg = isSvgFile(svgContent); if (!isSvg) { throw new Error("Invalid SVG content"); } const filterOptions = { ...svgSanitizeOptions, ...(svgOptions || {}) }; if (options?.preserveCase) { filterOptions.onTagAttr = preserveAttrCase; } const processedContent = advanceProcess(svgContent, filterOptions); return options?.preserveCase ? preserveTagCase(processedContent) : processedContent; }; 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;