UNPKG

@bliztek/feed-generator

Version:

A simple and lightweight Node.js library for generating RSS 2.0, Atom, and JSON Feed formats.

78 lines (77 loc) 3.57 kB
const ALLOWED_TAGS = new Set([ "a", "abbr", "b", "blockquote", "br", "caption", "cite", "code", "dd", "del", "details", "dfn", "div", "dl", "dt", "em", "figcaption", "figure", "h1", "h2", "h3", "h4", "h5", "h6", "hr", "i", "img", "ins", "kbd", "li", "mark", "ol", "p", "picture", "pre", "q", "s", "samp", "small", "source", "span", "strong", "sub", "summary", "sup", "table", "tbody", "td", "tfoot", "th", "thead", "time", "tr", "u", "ul", "var", "wbr", ]); const ALLOWED_ATTRS = { a: new Set(["href", "title", "rel", "target"]), img: new Set(["src", "alt", "title", "width", "height", "loading"]), source: new Set(["src", "srcset", "type", "media"]), td: new Set(["colspan", "rowspan"]), th: new Set(["colspan", "rowspan", "scope"]), ol: new Set(["start", "type", "reversed"]), time: new Set(["datetime"]), blockquote: new Set(["cite"]), q: new Set(["cite"]), del: new Set(["cite", "datetime"]), ins: new Set(["cite", "datetime"]), }; // Atomic-style patterns to prevent ReDoS (no overlapping alternatives) const EVENT_HANDLER = /\s+on[a-z]+\s*=\s*"[^"]*"/gi; const EVENT_HANDLER_SQ = /\s+on[a-z]+\s*=\s*'[^']*'/gi; const EVENT_HANDLER_UQ = /\s+on[a-z]+\s*=\s*[^\s>"']+/gi; const DANGEROUS_URL = /\s+(href|src|srcset|action)\s*=\s*["']\s*(javascript|data|vbscript)\s*:[^"']*/gi; const DANGEROUS_URL_UQ = /\s+(href|src|srcset|action)\s*=\s*(javascript|data|vbscript)\s*:[^\s>]*/gi; const CONTENT_STRIP_TAGS = ["script", "style", "svg", "math", "iframe", "object", "embed", "applet"]; const stripTagContent = (html, tag) => { const pattern = new RegExp(`<\\s*${tag}\\b[^>]*>[\\s\\S]*?<\\s*/\\s*${tag}\\s*>`, "gi"); return html.replace(pattern, ""); }; const stripDisallowedTags = (html) => html.replace(/<\s*\/?\s*(\w[\w-]*)\b[^>]*\/?>/gi, (match, tagName) => { if (ALLOWED_TAGS.has(tagName.toLowerCase())) return match; return ""; }); const stripEventHandlers = (html) => html .replace(EVENT_HANDLER, "") .replace(EVENT_HANDLER_SQ, "") .replace(EVENT_HANDLER_UQ, ""); const stripDangerousUrls = (html) => html .replace(DANGEROUS_URL, "") .replace(DANGEROUS_URL_UQ, ""); export const sanitizeHtml = (html) => { let result = html; // 1. Strip tags whose content is dangerous for (const tag of CONTENT_STRIP_TAGS) { result = stripTagContent(result, tag); } // 2. Strip event handlers and dangerous URLs result = stripEventHandlers(result); result = stripDangerousUrls(result); // 3. Strip disallowed tags (keeps text content between open/close) result = stripDisallowedTags(result); // 4. Filter attributes on remaining tags result = result.replace(/<(\w[\w-]*)((?:\s+[^>]*?)?)(\s*\/?)>/gi, (_match, tagName, attrsStr, close) => { const tag = tagName.toLowerCase(); const allowed = ALLOWED_ATTRS[tag]; if (!attrsStr.trim()) return `<${tagName}${close}>`; if (!allowed) return `<${tagName}${close}>`; const filtered = attrsStr.replace(/\s+(\w[\w-]*)\s*=\s*("[^"]*"|'[^']*'|[^\s>]+)/g, (attrMatch, name, value) => { if (!allowed.has(name.toLowerCase())) return ""; // Block dangerous URLs in allowed attributes const unquoted = value.replace(/^["']|["']$/g, "").trim(); if (/^\s*(javascript|data|vbscript)\s*:/i.test(unquoted)) return ""; return attrMatch; }); return `<${tagName}${filtered}${close}>`; }); return result; };