mirror of
https://github.com/tdurieux/anonymous_github.git
synced 2026-10-01 22:29:47 +02:00
Sanitize markdown HTML output with DOMPurify to prevent XSS (#658)
This commit is contained in:
1 parent
b2d77faa6c
commit
812f8b6314
6 files changed
+1299
-3
No files matched your search
Generated
+1028
File diff suppressed because it is too large.
Load diff
@@ -58,6 +58,7 @@
|
|||||||
"express-slow-down": "^2.0.1",
|
"express-slow-down": "^2.0.1",
|
||||||
"got": "^11.8.6",
|
"got": "^11.8.6",
|
||||||
"inquirer": "^8.2.6",
|
"inquirer": "^8.2.6",
|
||||||
|
"isomorphic-dompurify": "^3.8.0",
|
||||||
"istextorbinary": "^9.5.0",
|
"istextorbinary": "^9.5.0",
|
||||||
"marked": "^5.1.2",
|
"marked": "^5.1.2",
|
||||||
"mime-types": "^2.1.35",
|
"mime-types": "^2.1.35",
|
||||||
@@ -86,6 +87,7 @@
|
|||||||
"@types/passport": "^1.0.16",
|
"@types/passport": "^1.0.16",
|
||||||
"@types/passport-github2": "^1.2.9",
|
"@types/passport-github2": "^1.2.9",
|
||||||
"@types/unzip-stream": "^0.3.4",
|
"@types/unzip-stream": "^0.3.4",
|
||||||
|
"chai": "^4.5.0",
|
||||||
"gulp": "^5.0.0",
|
"gulp": "^5.0.0",
|
||||||
"gulp-clean-css": "^4.3.0",
|
"gulp-clean-css": "^4.3.0",
|
||||||
"gulp-concat": "^2.6.1",
|
"gulp-concat": "^2.6.1",
|
||||||
|
|||||||
Vendored
+1
-1
@@ -6,4 +6,4 @@ ${e}</blockquote>
|
|||||||
`}table(e,t){return"<table>\n<thead>\n"+e+"</thead>\n"+(t=t&&`<tbody>${t}</tbody>`)+"</table>\n"}tablerow(e){return`<tr>
|
`}table(e,t){return"<table>\n<thead>\n"+e+"</thead>\n"+(t=t&&`<tbody>${t}</tbody>`)+"</table>\n"}tablerow(e){return`<tr>
|
||||||
${e}</tr>
|
${e}</tr>
|
||||||
`}tablecell(e,t){var i=t.header?"th":"td";return(t.align?`<${i} align="${t.align}">`:`<${i}>`)+e+`</${i}>
|
`}tablecell(e,t){var i=t.header?"th":"td";return(t.align?`<${i} align="${t.align}">`:`<${i}>`)+e+`</${i}>
|
||||||
`}strong(e){return`<strong>${e}</strong>`}em(e){return`<em>${e}</em>`}codespan(e){return`<code>${e}</code>`}br(){return"<br>"}del(e){return`<del>${e}</del>`}link(e,t,i){var n=c(e);if(null===n)return i;let s='<a href="'+(e=n)+'"';return t&&(s+=' title="'+t+'"'),s+=">"+i+"</a>"}image(e,t,i){var n=c(e);if(null===n)return i;let s=`<img src="${e=n}" alt="${i}"`;return t&&(s+=` title="${t}"`),s+=">"}text(e){return e}}class R{strong(e){return e}em(e){return e}codespan(e){return e}del(e){return e}html(e){return e}text(e){return e}link(e,t,i){return""+i}image(e,t,i){return""+i}br(){return""}}class M{options;renderer;textRenderer;constructor(e){this.options=e||t.defaults,this.options.renderer=this.options.renderer||new E,this.renderer=this.options.renderer,this.renderer.options=this.options,this.textRenderer=new R}static parse(e,t){return new M(t).parse(e)}static parseInline(e,t){return new M(t).parseInline(e)}parse(n,s=!0){let o="";for(let i=0;i<n.length;i++){var r=n[i];if(this.options.extensions&&this.options.extensions.renderers&&this.options.extensions.renderers[r.type]){const n=r,s=this.options.extensions.renderers[n.type].call({parser:this},n);if(!1!==s||!["space","hr","heading","code","table","blockquote","list","html","paragraph","text"].includes(n.type)){o+=s||"";continue}}switch(r.type){case"space":continue;case"hr":o+=this.renderer.hr();continue;case"heading":{const n=r;o+=this.renderer.heading(this.parseInline(n.tokens),n.depth,this.parseInline(n.tokens,this.textRenderer).replace(F,(e,t)=>"colon"===(t=t.toLowerCase())?":":"#"===t.charAt(0)?"x"===t.charAt(1)?String.fromCharCode(parseInt(t.substring(2),16)):String.fromCharCode(+t.substring(1)):""));continue}case"code":{const n=r;o+=this.renderer.code(n.text,n.lang,!!n.escaped);continue}case"table":{const n=r;let e="",t="";for(let e=0;e<n.header.length;e++)t+=this.renderer.tablecell(this.parseInline(n.header[e].tokens),{header:!0,align:n.align[e]});e+=this.renderer.tablerow(t);let i="";for(let e=0;e<n.rows.length;e++){const o=n.rows[e];t="";for(let e=0;e<o.length;e++)t+=this.renderer.tablecell(this.parseInline(o[e].tokens),{header:!1,align:n.align[e]});i+=this.renderer.tablerow(t)}o+=this.renderer.table(e,i);continue}case"blockquote":{const n=r,s=this.parse(n.tokens);o+=this.renderer.blockquote(s);continue}case"list":{const n=r,s=n.ordered,e=n.start,a=n.loose;let i="";for(let t=0;t<n.items.length;t++){const o=n.items[t],c=o.checked,l=o.task;let e="";if(o.task){const n=this.renderer.checkbox(!!c);a?0<o.tokens.length&&"paragraph"===o.tokens[0].type?(o.tokens[0].text=n+" "+o.tokens[0].text,o.tokens[0].tokens&&0<o.tokens[0].tokens.length&&"text"===o.tokens[0].tokens[0].type&&(o.tokens[0].tokens[0].text=n+" "+o.tokens[0].tokens[0].text)):o.tokens.unshift({type:"text",text:n+" "}):e+=n+" "}e+=this.parse(o.tokens,a),i+=this.renderer.listitem(e,l,!!c)}o+=this.renderer.list(i,s,e);continue}case"html":{const n=r;o+=this.renderer.html(n.text,n.block);continue}case"paragraph":{const n=r;o+=this.renderer.paragraph(this.parseInline(n.tokens));continue}case"text":{let e=r,t=e.tokens?this.parseInline(e.tokens):e.text;for(;i+1<n.length&&"text"===n[i+1].type;)e=n[++i],t+="\n"+(e.tokens?this.parseInline(e.tokens):e.text);o+=s?this.renderer.paragraph(t):t;continue}default:{const n='Token with "'+r.type+'" type was not found.';if(this.options.silent)return console.error(n),"";throw new Error(n)}}}return o}parseInline(t,i){i=i||this.renderer;let n="";for(let e=0;e<t.length;e++){var s=t[e];if(this.options.extensions&&this.options.extensions.renderers&&this.options.extensions.renderers[s.type]){const t=this.options.extensions.renderers[s.type].call({parser:this},s);if(!1!==t||!["escape","html","link","image","strong","em","codespan","br","del","text"].includes(s.type)){n+=t||"";continue}}switch(s.type){case"escape":{const t=s;n+=i.text(t.text);break}case"html":{const t=s;n+=i.html(t.text);break}case"link":{const t=s;n+=i.link(t.href,t.title,this.parseInline(t.tokens,i));break}case"image":{const t=s;n+=i.image(t.href,t.title,t.text);break}case"strong":{const t=s;n+=i.strong(this.parseInline(t.tokens,i));break}case"em":{const t=s;n+=i.em(this.parseInline(t.tokens,i));break}case"codespan":{const t=s;n+=i.codespan(t.text);break}case"br":n+=i.br();break;case"del":{const t=s;n+=i.del(this.parseInline(t.tokens,i));break}case"text":{const t=s;n+=i.text(t.text);break}default:{const t='Token with "'+s.type+'" type was not found.';if(this.options.silent)return console.error(t),"";throw new Error(t)}}}return n}}class L{options;constructor(e){this.options=e||t.defaults}static passThroughHooks=new Set(["preprocess","postprocess","processAllTokens"]);preprocess(e){return e}postprocess(e){return e}processAllTokens(e){return e}}class ce{defaults={async:!1,breaks:!1,extensions:null,gfm:!0,hooks:null,pedantic:!1,renderer:null,silent:!1,tokenizer:null,walkTokens:null};options=this.setOptions;parse=this.#e(j.lex,M.parse);parseInline=this.#e(j.lexInline,M.parseInline);Parser=M;Renderer=E;TextRenderer=RLine truncated
|
`}strong(e){return`<strong>${e}</strong>`}em(e){return`<em>${e}</em>`}codespan(e){return`<code>${e}</code>`}br(){return"<br>"}del(e){return`<del>${e}</del>`}link(e,t,i){var n=c(e);if(null===n)return i;let s='<a href="'+(e=n)+'"';return t&&(s+=' title="'+t+'"'),s+=">"+i+"</a>"}image(e,t,i){var n=c(e);if(null===n)return i;let s=`<img src="${e=n}" alt="${i}"`;return t&&(s+=` title="${t}"`),s+=">"}text(e){return e}}class R{strong(e){return e}em(e){return e}codespan(e){return e}del(e){return e}html(e){return e}text(e){return e}link(e,t,i){return""+i}image(e,t,i){return""+i}br(){return""}}class M{options;renderer;textRenderer;constructor(e){this.options=e||t.defaults,this.options.renderer=this.options.renderer||new E,this.renderer=this.options.renderer,this.renderer.options=this.options,this.textRenderer=new R}static parse(e,t){return new M(t).parse(e)}static parseInline(e,t){return new M(t).parseInline(e)}parse(n,s=!0){let o="";for(let i=0;i<n.length;i++){var r=n[i];if(this.options.extensions&&this.options.extensions.renderers&&this.options.extensions.renderers[r.type]){const n=r,s=this.options.extensions.renderers[n.type].call({parser:this},n);if(!1!==s||!["space","hr","heading","code","table","blockquote","list","html","paragraph","text"].includes(n.type)){o+=s||"";continue}}switch(r.type){case"space":continue;case"hr":o+=this.renderer.hr();continue;case"heading":{const n=r;o+=this.renderer.heading(this.parseInline(n.tokens),n.depth,this.parseInline(n.tokens,this.textRenderer).replace(F,(e,t)=>"colon"===(t=t.toLowerCase())?":":"#"===t.charAt(0)?"x"===t.charAt(1)?String.fromCharCode(parseInt(t.substring(2),16)):String.fromCharCode(+t.substring(1)):""));continue}case"code":{const n=r;o+=this.renderer.code(n.text,n.lang,!!n.escaped);continue}case"table":{const n=r;let e="",t="";for(let e=0;e<n.header.length;e++)t+=this.renderer.tablecell(this.parseInline(n.header[e].tokens),{header:!0,align:n.align[e]});e+=this.renderer.tablerow(t);let i="";for(let e=0;e<n.rows.length;e++){const o=n.rows[e];t="";for(let e=0;e<o.length;e++)t+=this.renderer.tablecell(this.parseInline(o[e].tokens),{header:!1,align:n.align[e]});i+=this.renderer.tablerow(t)}o+=this.renderer.table(e,i);continue}case"blockquote":{const n=r,s=this.parse(n.tokens);o+=this.renderer.blockquote(s);continue}case"list":{const n=r,s=n.ordered,e=n.start,a=n.loose;let i="";for(let t=0;t<n.items.length;t++){const o=n.items[t],c=o.checked,l=o.task;let e="";if(o.task){const n=this.renderer.checkbox(!!c);a?0<o.tokens.length&&"paragraph"===o.tokens[0].type?(o.tokens[0].text=n+" "+o.tokens[0].text,o.tokens[0].tokens&&0<o.tokens[0].tokens.length&&"text"===o.tokens[0].tokens[0].type&&(o.tokens[0].tokens[0].text=n+" "+o.tokens[0].tokens[0].text)):o.tokens.unshift({type:"text",text:n+" "}):e+=n+" "}e+=this.parse(o.tokens,a),i+=this.renderer.listitem(e,l,!!c)}o+=this.renderer.list(i,s,e);continue}case"html":{const n=r;o+=this.renderer.html(n.text,n.block);continue}case"paragraph":{const n=r;o+=this.renderer.paragraph(this.parseInline(n.tokens));continue}case"text":{let e=r,t=e.tokens?this.parseInline(e.tokens):e.text;for(;i+1<n.length&&"text"===n[i+1].type;)e=n[++i],t+="\n"+(e.tokens?this.parseInline(e.tokens):e.text);o+=s?this.renderer.paragraph(t):t;continue}default:{const n='Token with "'+r.type+'" type was not found.';if(this.options.silent)return console.error(n),"";throw new Error(n)}}}return o}parseInline(t,i){i=i||this.renderer;let n="";for(let e=0;e<t.length;e++){var s=t[e];if(this.options.extensions&&this.options.extensions.renderers&&this.options.extensions.renderers[s.type]){const t=this.options.extensions.renderers[s.type].call({parser:this},s);if(!1!==t||!["escape","html","link","image","strong","em","codespan","br","del","text"].includes(s.type)){n+=t||"";continue}}switch(s.type){case"escape":{const t=s;n+=i.text(t.text);break}case"html":{const t=s;n+=i.html(t.text);break}case"link":{const t=s;n+=i.link(t.href,t.title,this.parseInline(t.tokens,i));break}case"image":{const t=s;n+=i.image(t.href,t.title,t.text);break}case"strong":{const t=s;n+=i.strong(this.parseInline(t.tokens,i));break}case"em":{const t=s;n+=i.em(this.parseInline(t.tokens,i));break}case"codespan":{const t=s;n+=i.codespan(t.text);break}case"br":n+=i.br();break;case"del":{const t=s;n+=i.del(this.parseInline(t.tokens,i));break}case"text":{const t=s;n+=i.text(t.text);break}default:{const t='Token with "'+s.type+'" type was not found.';if(this.options.silent)return console.error(t),"";throw new Error(t)}}}return n}}class L{options;constructor(e){this.options=e||t.defaults}static passThroughHooks=new Set(["preprocess","postprocess","processAllTokens"]);preprocess(e){return e}postprocess(e){return e}processAllTokens(e){return e}}class ce{defaults={async:!1,breaks:!1,extensions:null,gfm:!0,hooks:null,pedantic:!1,renderer:null,silent:!1,tokenizer:null,walkTokens:null};options=this.setOptions;parse=this.#e(j.lex,M.parse);parseInline=this.#e(j.lexInline,M.parseInline);Parser=M;Renderer=E;TextRenderer=RLine truncated
|
||||||
@@ -182,5 +182,5 @@ function renderMD(md, baseUrlValue) {
|
|||||||
throwOnError: false,
|
throwOnError: false,
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
return marked.parse(md, { renderer });
|
return DOMPurify.sanitize(marked.parse(md, { renderer }));
|
||||||
}
|
}
|
||||||
@@ -4,6 +4,7 @@ import * as path from "path";
|
|||||||
import AnonymizedFile from "../../core/AnonymizedFile";
|
import AnonymizedFile from "../../core/AnonymizedFile";
|
||||||
import AnonymousError from "../../core/AnonymousError";
|
import AnonymousError from "../../core/AnonymousError";
|
||||||
import * as marked from "marked";
|
import * as marked from "marked";
|
||||||
|
import DOMPurify from "isomorphic-dompurify";
|
||||||
import { streamToString } from "../../core/anonymize-utils";
|
import { streamToString } from "../../core/anonymize-utils";
|
||||||
import { IFile } from "../../core/model/files/files.types";
|
import { IFile } from "../../core/model/files/files.types";
|
||||||
|
|
||||||
@@ -113,7 +114,7 @@ async function webView(req: express.Request, res: express.Response) {
|
|||||||
}
|
}
|
||||||
if (f.extension() == "md") {
|
if (f.extension() == "md") {
|
||||||
const content = await streamToString(await f.anonymizedContent());
|
const content = await streamToString(await f.anonymizedContent());
|
||||||
const body = marked.marked(content, { headerIds: false, mangle: false });
|
const body = DOMPurify.sanitize(marked.marked(content, { headerIds: false, mangle: false }));
|
||||||
const html = `<!DOCTYPE html><html><head><title>Content</title></head><link rel="stylesheet" href="/css/all.min.css" /><body><div class="container p-3 file-content markdown-body">${body}<div></body></html>`;
|
const html = `<!DOCTYPE html><html><head><title>Content</title></head><link rel="stylesheet" href="/css/all.min.css" /><body><div class="container p-3 file-content markdown-body">${body}<div></body></html>`;
|
||||||
res.contentType("text/html").send(html);
|
res.contentType("text/html").send(html);
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -0,0 +1,265 @@
|
|||||||
|
const { expect } = require("chai");
|
||||||
|
const { marked } = require("marked");
|
||||||
|
const DOMPurify = require("isomorphic-dompurify");
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper that mirrors the server-side rendering pipeline in webview.ts:
|
||||||
|
* DOMPurify.sanitize(marked.marked(content, { headerIds: false, mangle: false }))
|
||||||
|
*/
|
||||||
|
function renderAndSanitize(markdown) {
|
||||||
|
const raw = marked(markdown, { headerIds: false, mangle: false });
|
||||||
|
return DOMPurify.sanitize(raw);
|
||||||
|
}
|
||||||
|
|
||||||
|
describe("Markdown sanitization", function () {
|
||||||
|
// ---------------------------------------------------------------
|
||||||
|
// Script injection
|
||||||
|
// ---------------------------------------------------------------
|
||||||
|
describe("removes script tags", function () {
|
||||||
|
it("strips inline <script> tags", function () {
|
||||||
|
const html = renderAndSanitize('<script>alert("xss")</script>');
|
||||||
|
expect(html).to.not.include("<script");
|
||||||
|
expect(html).to.not.include("alert(");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("strips script tags with src attribute", function () {
|
||||||
|
const html = renderAndSanitize(
|
||||||
|
'<script src="https://evil.com/xss.js"></script>'
|
||||||
|
);
|
||||||
|
expect(html).to.not.include("<script");
|
||||||
|
expect(html).to.not.include("evil.com");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("strips script tags embedded in markdown", function () {
|
||||||
|
const html = renderAndSanitize(
|
||||||
|
"# Hello\n\n<script>document.cookie</script>\n\nWorld"
|
||||||
|
);
|
||||||
|
expect(html).to.not.include("<script");
|
||||||
|
expect(html).to.include("Hello");
|
||||||
|
expect(html).to.include("World");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------
|
||||||
|
// Event handler injection
|
||||||
|
// ---------------------------------------------------------------
|
||||||
|
describe("removes event handlers", function () {
|
||||||
|
it("strips onerror handler on img", function () {
|
||||||
|
const html = renderAndSanitize('<img src=x onerror="alert(1)">');
|
||||||
|
expect(html).to.not.include("onerror");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("strips onload handler on img", function () {
|
||||||
|
const html = renderAndSanitize(
|
||||||
|
'<img src="valid.png" onload="alert(1)">'
|
||||||
|
);
|
||||||
|
expect(html).to.not.include("onload");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("strips onmouseover handler on a tag", function () {
|
||||||
|
const html = renderAndSanitize(
|
||||||
|
'<a href="#" onmouseover="alert(1)">hover me</a>'
|
||||||
|
);
|
||||||
|
expect(html).to.not.include("onmouseover");
|
||||||
|
expect(html).to.include("hover me");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("strips onfocus handler on input", function () {
|
||||||
|
const html = renderAndSanitize('<input onfocus="alert(1)" autofocus>');
|
||||||
|
expect(html).to.not.include("onfocus");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------
|
||||||
|
// javascript: URLs
|
||||||
|
// ---------------------------------------------------------------
|
||||||
|
describe("removes javascript: URLs", function () {
|
||||||
|
it("strips javascript: href in anchor", function () {
|
||||||
|
const html = renderAndSanitize(
|
||||||
|
'<a href="javascript:alert(1)">click</a>'
|
||||||
|
);
|
||||||
|
expect(html).to.not.include("javascript:");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("strips javascript: href in markdown link syntax", function () {
|
||||||
|
const html = renderAndSanitize("[click](javascript:alert(1))");
|
||||||
|
expect(html).to.not.include("javascript:");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------
|
||||||
|
// iframe / object / embed
|
||||||
|
// ---------------------------------------------------------------
|
||||||
|
describe("removes dangerous elements", function () {
|
||||||
|
it("strips iframe", function () {
|
||||||
|
const html = renderAndSanitize(
|
||||||
|
'<iframe src="https://evil.com"></iframe>'
|
||||||
|
);
|
||||||
|
expect(html).to.not.include("<iframe");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("strips object tag", function () {
|
||||||
|
const html = renderAndSanitize(
|
||||||
|
'<object data="malware.swf"></object>'
|
||||||
|
);
|
||||||
|
expect(html).to.not.include("<object");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("strips embed tag", function () {
|
||||||
|
const html = renderAndSanitize('<embed src="malware.swf">');
|
||||||
|
expect(html).to.not.include("<embed");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("strips form action with javascript: URL", function () {
|
||||||
|
const html = renderAndSanitize(
|
||||||
|
'<form action="javascript:alert(1)"><input type="submit"></form>'
|
||||||
|
);
|
||||||
|
expect(html).to.not.include("javascript:");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------
|
||||||
|
// SVG-based attacks
|
||||||
|
// ---------------------------------------------------------------
|
||||||
|
describe("removes SVG-based XSS", function () {
|
||||||
|
it("strips svg with onload", function () {
|
||||||
|
const html = renderAndSanitize('<svg onload="alert(1)">');
|
||||||
|
expect(html).to.not.include("onload");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("strips svg with embedded script", function () {
|
||||||
|
const html = renderAndSanitize(
|
||||||
|
"<svg><script>alert(1)</script></svg>"
|
||||||
|
);
|
||||||
|
expect(html).to.not.include("<script");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------
|
||||||
|
// data: URL attacks
|
||||||
|
// ---------------------------------------------------------------
|
||||||
|
describe("removes data: URL attacks", function () {
|
||||||
|
it("strips data:text/html href", function () {
|
||||||
|
const html = renderAndSanitize(
|
||||||
|
'<a href="data:text/html,<script>alert(1)</script>">click</a>'
|
||||||
|
);
|
||||||
|
expect(html).to.not.include("data:text/html");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------
|
||||||
|
// style-based attacks
|
||||||
|
// ---------------------------------------------------------------
|
||||||
|
describe("removes style-based attacks", function () {
|
||||||
|
it("strips style tags with expressions", function () {
|
||||||
|
const html = renderAndSanitize(
|
||||||
|
"<style>body { background: url('javascript:alert(1)') }</style>"
|
||||||
|
);
|
||||||
|
expect(html).to.not.include("javascript:");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------
|
||||||
|
// Safe content is preserved
|
||||||
|
// ---------------------------------------------------------------
|
||||||
|
describe("preserves safe markdown content", function () {
|
||||||
|
it("preserves headings", function () {
|
||||||
|
const html = renderAndSanitize("# Heading 1\n## Heading 2");
|
||||||
|
expect(html).to.include("<h1>");
|
||||||
|
expect(html).to.include("Heading 1");
|
||||||
|
expect(html).to.include("<h2>");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("preserves paragraphs", function () {
|
||||||
|
const html = renderAndSanitize("Hello world\n\nSecond paragraph");
|
||||||
|
expect(html).to.include("<p>");
|
||||||
|
expect(html).to.include("Hello world");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("preserves bold and italic", function () {
|
||||||
|
const html = renderAndSanitize("**bold** and *italic*");
|
||||||
|
expect(html).to.include("<strong>bold</strong>");
|
||||||
|
expect(html).to.include("<em>italic</em>");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("preserves links", function () {
|
||||||
|
const html = renderAndSanitize("[example](https://example.com)");
|
||||||
|
expect(html).to.include("https://example.com");
|
||||||
|
expect(html).to.include("example");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("preserves images", function () {
|
||||||
|
const html = renderAndSanitize(
|
||||||
|
""
|
||||||
|
);
|
||||||
|
expect(html).to.include("<img");
|
||||||
|
expect(html).to.include("https://example.com/img.png");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("preserves code blocks", function () {
|
||||||
|
const html = renderAndSanitize("```js\nconsole.log('hi')\n```");
|
||||||
|
expect(html).to.include("<code");
|
||||||
|
expect(html).to.include("console.log");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("preserves inline code", function () {
|
||||||
|
const html = renderAndSanitize("Use `npm install` to install");
|
||||||
|
expect(html).to.include("<code>npm install</code>");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("preserves unordered lists", function () {
|
||||||
|
const html = renderAndSanitize("- item 1\n- item 2\n- item 3");
|
||||||
|
expect(html).to.include("<ul>");
|
||||||
|
expect(html).to.include("<li>");
|
||||||
|
expect(html).to.include("item 1");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("preserves ordered lists", function () {
|
||||||
|
const html = renderAndSanitize("1. first\n2. second");
|
||||||
|
expect(html).to.include("<ol>");
|
||||||
|
expect(html).to.include("first");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("preserves blockquotes", function () {
|
||||||
|
const html = renderAndSanitize("> This is a quote");
|
||||||
|
expect(html).to.include("<blockquote>");
|
||||||
|
expect(html).to.include("This is a quote");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("preserves tables", function () {
|
||||||
|
const html = renderAndSanitize("| A | B |\n|---|---|\n| 1 | 2 |");
|
||||||
|
expect(html).to.include("<table>");
|
||||||
|
expect(html).to.include("<th>");
|
||||||
|
expect(html).to.include("<td>");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("preserves horizontal rules", function () {
|
||||||
|
const html = renderAndSanitize("---");
|
||||||
|
expect(html).to.include("<hr");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------
|
||||||
|
// Mixed: malicious + safe content
|
||||||
|
// ---------------------------------------------------------------
|
||||||
|
describe("handles mixed content", function () {
|
||||||
|
it("strips malicious parts while keeping safe parts", function () {
|
||||||
|
const html = renderAndSanitize(
|
||||||
|
'# Title\n\nSafe paragraph.\n\n<script>alert("xss")</script>\n\n**Bold text**'
|
||||||
|
);
|
||||||
|
expect(html).to.not.include("<script");
|
||||||
|
expect(html).to.include("Title");
|
||||||
|
expect(html).to.include("Safe paragraph");
|
||||||
|
expect(html).to.include("<strong>Bold text</strong>");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("strips event handlers from otherwise-safe tags", function () {
|
||||||
|
const html = renderAndSanitize(
|
||||||
|
'<img src="photo.jpg" alt="photo" onerror="alert(1)">'
|
||||||
|
);
|
||||||
|
expect(html).to.not.include("onerror");
|
||||||
|
expect(html).to.include("photo.jpg");
|
||||||
|
expect(html).to.include('alt="photo"');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in new issue
Block a user