@bliztek/feed-generator
Version:
A simple and lightweight Node.js library for generating RSS 2.0, Atom, and JSON Feed formats.
82 lines (81 loc) • 3.71 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.sanitizeHtml = void 0;
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, "");
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;
};
exports.sanitizeHtml = sanitizeHtml;