mirror of
https://github.com/rr-/szurubooru.git
synced 2025-07-17 08:26:24 +00:00
Introduced in 0137cf383a
, anywhere that
allows markdown e.g. comments allowed any arbitrary tag that wasn't
explicitly banned by DOMPurify, since it cannot know whether it came
from our Markdown renderer or from the user.
People could add arbitrary <style> tags that mess with the page,
<button>s etc. It did not lead to XSS since DOMPurify strips it,
but still unacceptable.
escapeHtml is identical to the old behavior of marked.js sanitize=true
201 lines
5.1 KiB
JavaScript
201 lines
5.1 KiB
JavaScript
"use strict";
|
|
|
|
const marked = require("marked");
|
|
const DOMPurify = require("dompurify");
|
|
|
|
class BaseMarkdownWrapper {
|
|
preprocess(text) {
|
|
return text;
|
|
}
|
|
|
|
postprocess(text) {
|
|
return text;
|
|
}
|
|
}
|
|
|
|
class SjisWrapper extends BaseMarkdownWrapper {
|
|
constructor() {
|
|
super();
|
|
this.buf = [];
|
|
}
|
|
|
|
preprocess(text) {
|
|
return text.replace(
|
|
/\[sjis\]((?:[^\[]|\[(?!\/?sjis\]))+)\[\/sjis\]/gi,
|
|
(match, capture) => {
|
|
var ret = "%%%SJIS" + this.buf.length;
|
|
this.buf.push(capture);
|
|
return ret;
|
|
}
|
|
);
|
|
}
|
|
|
|
postprocess(text) {
|
|
return text.replace(
|
|
/(?:<p>)?%%%SJIS(\d+)(?:<\/p>)?/,
|
|
(match, capture) => {
|
|
return '<div class="sjis">' + this.buf[capture] + "</div>";
|
|
}
|
|
);
|
|
}
|
|
}
|
|
|
|
// fix \ before ~ being stripped away
|
|
class TildeWrapper extends BaseMarkdownWrapper {
|
|
preprocess(text) {
|
|
return text.replace(/\\~/g, "%%%T");
|
|
}
|
|
|
|
postprocess(text) {
|
|
return text.replace(/%%%T/g, "\\~");
|
|
}
|
|
}
|
|
|
|
// prevent ^#... from being treated as headers, due to tag permalinks
|
|
class TagPermalinkFixWrapper extends BaseMarkdownWrapper {
|
|
preprocess(text) {
|
|
return text.replace(/^#/g, "%%%#");
|
|
}
|
|
|
|
postprocess(text) {
|
|
return text.replace(/%%%#/g, "#");
|
|
}
|
|
}
|
|
|
|
// post, user and tags permalinks
|
|
class EntityPermalinkWrapper extends BaseMarkdownWrapper {
|
|
preprocess(text) {
|
|
text = text.replace(
|
|
/(^|^\(|(?:[^\]])\(|[\s<>\[\]\)])([+#@][a-zA-Z0-9_-]+)/g,
|
|
"$1[$2]($2)"
|
|
);
|
|
text = text.replace(/\]\(@(\d+)\)/g, "](/post/$1)");
|
|
text = text.replace(/\]\(\+([a-zA-Z0-9_-]+)\)/g, "](/user/$1)");
|
|
text = text.replace(/\]\(#([a-zA-Z0-9_-]+)\)/g, "](/posts/query=$1)");
|
|
return text;
|
|
}
|
|
}
|
|
|
|
class SearchPermalinkWrapper extends BaseMarkdownWrapper {
|
|
postprocess(text) {
|
|
return text.replace(
|
|
/\[search\]((?:[^\[]|\[(?!\/?search\]))+)\[\/search\]/gi,
|
|
'<a href="/posts/query=$1"><code>$1</code></a>'
|
|
);
|
|
}
|
|
}
|
|
|
|
class SpoilersWrapper extends BaseMarkdownWrapper {
|
|
postprocess(text) {
|
|
return text.replace(
|
|
/\[spoiler\]((?:[^\[]|\[(?!\/?spoiler\]))+)\[\/spoiler\]/gi,
|
|
'<span class="spoiler">$1</span>'
|
|
);
|
|
}
|
|
}
|
|
|
|
class SmallWrapper extends BaseMarkdownWrapper {
|
|
postprocess(text) {
|
|
return text.replace(
|
|
/\[small\]((?:[^\[]|\[(?!\/?small\]))+)\[\/small\]/gi,
|
|
"<small>$1</small>"
|
|
);
|
|
}
|
|
}
|
|
|
|
class StrikeThroughWrapper extends BaseMarkdownWrapper {
|
|
postprocess(text) {
|
|
text = text.replace(/(^|[^\\])(~~|~)([^~]+)\2/g, "$1<del>$3</del>");
|
|
return text.replace(/\\~/g, "~");
|
|
}
|
|
}
|
|
|
|
function escapeHtml(unsafe) {
|
|
return unsafe
|
|
.toString()
|
|
.replace(/&/g, "&")
|
|
.replace(/</g, "<")
|
|
.replace(/>/g, ">")
|
|
.replace(/"/g, """)
|
|
.replace(/'/g, "'");
|
|
}
|
|
|
|
function createRenderer() {
|
|
const renderer = new marked.Renderer();
|
|
renderer.image = (href, title, alt) => {
|
|
let [_, url, width, height] =
|
|
/^(.+?)(?:\s=\s*(\d*)\s*x\s*(\d*)\s*)?$/.exec(href);
|
|
let res = '<img src="' + escapeHtml(url) + '" alt="' + escapeHtml(alt);
|
|
if (width) {
|
|
res += '" width="' + width;
|
|
}
|
|
if (height) {
|
|
res += '" height="' + height;
|
|
}
|
|
return res + '">';
|
|
};
|
|
return renderer;
|
|
}
|
|
|
|
function formatMarkdown(text) {
|
|
const renderer = createRenderer();
|
|
const options = {
|
|
renderer: renderer,
|
|
breaks: true,
|
|
smartypants: true,
|
|
};
|
|
let wrappers = [
|
|
new SjisWrapper(),
|
|
new TildeWrapper(),
|
|
new TagPermalinkFixWrapper(),
|
|
new EntityPermalinkWrapper(),
|
|
new SearchPermalinkWrapper(),
|
|
new SpoilersWrapper(),
|
|
new SmallWrapper(),
|
|
new StrikeThroughWrapper(),
|
|
];
|
|
text = escapeHtml(text);
|
|
for (let wrapper of wrappers) {
|
|
text = wrapper.preprocess(text);
|
|
}
|
|
text = marked.parse(text, options);
|
|
wrappers.reverse();
|
|
for (let wrapper of wrappers) {
|
|
text = wrapper.postprocess(text);
|
|
}
|
|
return DOMPurify.sanitize(text);
|
|
}
|
|
|
|
function formatInlineMarkdown(text) {
|
|
const renderer = createRenderer();
|
|
const options = {
|
|
renderer: renderer,
|
|
breaks: true,
|
|
smartypants: true,
|
|
};
|
|
let wrappers = [
|
|
new TildeWrapper(),
|
|
new EntityPermalinkWrapper(),
|
|
new SearchPermalinkWrapper(),
|
|
new SpoilersWrapper(),
|
|
new SmallWrapper(),
|
|
new StrikeThroughWrapper(),
|
|
];
|
|
text = escapeHtml(text);
|
|
for (let wrapper of wrappers) {
|
|
text = wrapper.preprocess(text);
|
|
}
|
|
text = marked.parseInline(text, options);
|
|
wrappers.reverse();
|
|
for (let wrapper of wrappers) {
|
|
text = wrapper.postprocess(text);
|
|
}
|
|
return DOMPurify.sanitize(text);
|
|
}
|
|
|
|
module.exports = {
|
|
formatMarkdown: formatMarkdown,
|
|
formatInlineMarkdown: formatInlineMarkdown,
|
|
escapeHtml: escapeHtml,
|
|
};
|