UNPKG

@blocklet/xss

Version:

blocklet prevent xss attack

161 lines (160 loc) 4.57 kB
import * as xss from "xss"; import omit from "lodash/omit"; import path from "path"; 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: omit(xss.getDefaultWhiteList(), ignoreTagList), onIgnoreTag: function(tag, html, options) { if (ignoreTagMap[tag]) { return ""; } }, stripIgnoreTagBody: ["script"] }; export 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; }; 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}"`; } } }; export 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); }; export 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.extname(file.name).toLowerCase(); if (ext !== ".svg") { return false; } } if (file?.type) { if (!file.type.toLowerCase().includes("image/svg")) { return false; } } return true; };