2020-09-15 22:42:51 +08:00
|
|
|
import xss from "xss";
|
2021-03-17 21:11:40 +08:00
|
|
|
import escape from "discourse-common/lib/escape";
|
2016-06-15 02:31:51 +08:00
|
|
|
|
2024-06-28 20:21:31 +08:00
|
|
|
// Should match any <iframe> without a src attribute
|
|
|
|
const IFRAME_REGEXP =
|
|
|
|
/<iframe(?![^>]*\s+src\s*=)[^>]*>[\s\S]*?(<\/iframe\s*>|$)/gi;
|
|
|
|
|
2016-06-15 02:31:51 +08:00
|
|
|
function attr(name, value) {
|
2016-07-21 01:30:36 +08:00
|
|
|
if (value) {
|
|
|
|
return `${name}="${xss.escapeAttrValue(value)}"`;
|
|
|
|
}
|
|
|
|
|
|
|
|
return name;
|
2016-06-15 02:31:51 +08:00
|
|
|
}
|
|
|
|
|
2021-03-17 21:11:40 +08:00
|
|
|
export { escape };
|
2016-06-15 02:31:51 +08:00
|
|
|
|
2016-10-21 23:39:48 +08:00
|
|
|
export function hrefAllowed(href, extraHrefMatchers) {
|
2016-06-15 02:31:51 +08:00
|
|
|
// escape single quotes
|
|
|
|
href = href.replace(/'/g, "%27");
|
|
|
|
|
|
|
|
// absolute urls
|
|
|
|
if (/^(https?:)?\/\/[\w\.\-]+/i.test(href)) {
|
|
|
|
return href;
|
|
|
|
}
|
|
|
|
// relative urls
|
|
|
|
if (/^\/[\w\.\-]+/i.test(href)) {
|
|
|
|
return href;
|
|
|
|
}
|
|
|
|
// anchors
|
|
|
|
if (/^#[\w\.\-]+/i.test(href)) {
|
|
|
|
return href;
|
|
|
|
}
|
|
|
|
// mailtos
|
|
|
|
if (/^mailto:[\w\.\-@]+/i.test(href)) {
|
|
|
|
return href;
|
|
|
|
}
|
2016-10-21 23:39:48 +08:00
|
|
|
|
|
|
|
if (extraHrefMatchers && extraHrefMatchers.length > 0) {
|
|
|
|
for (let i = 0; i < extraHrefMatchers.length; i++) {
|
|
|
|
if (extraHrefMatchers[i].test(href)) {
|
|
|
|
return href;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2016-06-15 02:31:51 +08:00
|
|
|
}
|
|
|
|
|
2022-05-19 18:18:30 +08:00
|
|
|
function sanitizeMediaSrc(tag, attrName, value, extraHrefMatchers) {
|
|
|
|
const srcAttrs = {
|
|
|
|
img: ["src"],
|
|
|
|
source: ["src", "srcset"],
|
|
|
|
track: ["src"],
|
|
|
|
};
|
|
|
|
|
|
|
|
if (!srcAttrs[tag]?.includes(attrName)) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (value.startsWith("data:image")) {
|
|
|
|
return attr(attrName, value);
|
|
|
|
}
|
|
|
|
|
|
|
|
if (attrName === "srcset") {
|
|
|
|
const srcset = value.split(",").map((v) => v.split(" ", 2));
|
|
|
|
const sanitizedValue = srcset
|
|
|
|
.map((src) => {
|
|
|
|
const allowedSrc = hrefAllowed(src[0], extraHrefMatchers);
|
|
|
|
if (allowedSrc) {
|
|
|
|
return src[1] ? `${allowedSrc} ${src[1]}` : allowedSrc;
|
|
|
|
}
|
|
|
|
})
|
|
|
|
.join(",");
|
|
|
|
return attr(attrName, sanitizedValue);
|
|
|
|
} else {
|
|
|
|
const returnVal = hrefAllowed(value, extraHrefMatchers);
|
|
|
|
return attr(attrName, returnVal);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-02-09 18:23:44 +08:00
|
|
|
function testDataAttribute(forTag, name, value) {
|
|
|
|
return Object.keys(forTag).find((k) => {
|
|
|
|
const nameWithMatcher = `^${k.replace(/\*$/, "\\w+?")}`;
|
|
|
|
const validValues = forTag[k];
|
|
|
|
|
|
|
|
return (
|
|
|
|
new RegExp(nameWithMatcher).test(name) &&
|
|
|
|
(validValues.includes("*") ? true : validValues.includes(value))
|
|
|
|
);
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2020-10-28 10:22:06 +08:00
|
|
|
export function sanitize(text, allowLister) {
|
2020-09-22 22:28:28 +08:00
|
|
|
if (!text) {
|
|
|
|
return "";
|
|
|
|
}
|
2016-06-15 02:31:51 +08:00
|
|
|
|
|
|
|
// Allow things like <3 and <_<
|
|
|
|
text = text.replace(/<([^A-Za-z\/\!]|$)/g, "<$1");
|
|
|
|
|
2020-10-28 10:22:06 +08:00
|
|
|
const allowList = allowLister.getAllowList(),
|
|
|
|
allowedHrefSchemes = allowLister.getAllowedHrefSchemes(),
|
|
|
|
allowedIframes = allowLister.getAllowedIframes();
|
2016-10-21 23:39:48 +08:00
|
|
|
let extraHrefMatchers = null;
|
|
|
|
|
|
|
|
if (allowedHrefSchemes && allowedHrefSchemes.length > 0) {
|
|
|
|
extraHrefMatchers = [
|
|
|
|
new RegExp("^(" + allowedHrefSchemes.join("|") + ")://[\\w\\.\\-]+", "i"),
|
|
|
|
];
|
2018-01-30 08:02:23 +08:00
|
|
|
if (allowedHrefSchemes.includes("tel")) {
|
|
|
|
extraHrefMatchers.push(new RegExp("^tel://\\+?[\\w\\.\\-]+", "i"));
|
|
|
|
}
|
2016-10-21 23:39:48 +08:00
|
|
|
}
|
2016-06-15 02:31:51 +08:00
|
|
|
|
|
|
|
let result = xss(text, {
|
2022-04-07 04:49:13 +08:00
|
|
|
allowList: allowList.tagList,
|
2016-06-15 02:31:51 +08:00
|
|
|
stripIgnoreTag: true,
|
|
|
|
stripIgnoreTagBody: ["script", "table"],
|
2016-07-05 02:15:51 +08:00
|
|
|
|
2016-06-15 02:31:51 +08:00
|
|
|
onIgnoreTagAttr(tag, name, value) {
|
2020-10-28 10:22:06 +08:00
|
|
|
const forTag = allowList.attrList[tag];
|
2016-06-15 02:31:51 +08:00
|
|
|
if (forTag) {
|
|
|
|
const forAttr = forTag[name];
|
2022-02-09 18:23:44 +08:00
|
|
|
|
2017-09-01 22:15:34 +08:00
|
|
|
if (
|
2022-07-18 02:48:36 +08:00
|
|
|
(forAttr && (forAttr.includes("*") || forAttr.includes(value))) ||
|
|
|
|
(!name.includes("data-html-") &&
|
2022-07-18 02:16:39 +08:00
|
|
|
name.startsWith("data-") &&
|
2022-02-09 18:23:44 +08:00
|
|
|
(forTag["data-*"] || testDataAttribute(forTag, name, value))) ||
|
2016-10-21 23:39:48 +08:00
|
|
|
(tag === "a" &&
|
|
|
|
name === "href" &&
|
|
|
|
hrefAllowed(value, extraHrefMatchers)) ||
|
2017-09-01 22:15:34 +08:00
|
|
|
(tag === "iframe" &&
|
|
|
|
name === "src" &&
|
2023-05-24 18:44:18 +08:00
|
|
|
!value.match(/\/\.+\//) &&
|
2017-09-01 22:15:34 +08:00
|
|
|
allowedIframes.some((i) => {
|
2022-07-18 02:16:39 +08:00
|
|
|
return value.toLowerCase().startsWith((i || "").toLowerCase());
|
2017-09-01 22:15:34 +08:00
|
|
|
}))
|
|
|
|
) {
|
2016-06-15 02:31:51 +08:00
|
|
|
return attr(name, value);
|
|
|
|
}
|
2022-05-19 18:18:30 +08:00
|
|
|
|
|
|
|
const sanitizedMediaSrc = sanitizeMediaSrc(
|
|
|
|
tag,
|
|
|
|
name,
|
|
|
|
value,
|
|
|
|
extraHrefMatchers
|
|
|
|
);
|
|
|
|
if (sanitizedMediaSrc) {
|
|
|
|
return sanitizedMediaSrc;
|
|
|
|
}
|
2016-06-15 02:31:51 +08:00
|
|
|
|
|
|
|
if (tag === "iframe" && name === "src") {
|
2024-06-28 20:21:31 +08:00
|
|
|
// This iframe is not allowed
|
|
|
|
return "";
|
2016-06-15 02:31:51 +08:00
|
|
|
}
|
|
|
|
|
2020-12-22 01:55:00 +08:00
|
|
|
if (tag === "video" && name === "autoplay") {
|
2021-05-21 09:43:47 +08:00
|
|
|
// This might give us duplicate 'muted' attributes
|
2020-12-22 01:55:00 +08:00
|
|
|
// but they will be deduped by later processing
|
|
|
|
return "autoplay muted";
|
|
|
|
}
|
|
|
|
|
2017-10-16 23:53:47 +08:00
|
|
|
// Heading ids must begin with `heading--`
|
|
|
|
if (
|
2022-07-18 02:48:36 +08:00
|
|
|
["h1", "h2", "h3", "h4", "h5", "h6"].includes(tag) &&
|
2017-10-16 23:53:47 +08:00
|
|
|
value.match(/^heading\-\-[a-zA-Z0-9\-\_]+$/)
|
|
|
|
) {
|
|
|
|
return attr(name, value);
|
|
|
|
}
|
|
|
|
|
2020-10-28 10:22:06 +08:00
|
|
|
const custom = allowLister.getCustom();
|
2016-06-15 02:31:51 +08:00
|
|
|
for (let i = 0; i < custom.length; i++) {
|
|
|
|
const fn = custom[i];
|
|
|
|
if (fn(tag, name, value)) {
|
|
|
|
return attr(name, value);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
},
|
|
|
|
});
|
|
|
|
|
|
|
|
return result
|
|
|
|
.replace(/\[removed\]/g, "")
|
2024-06-28 20:21:31 +08:00
|
|
|
.replace(IFRAME_REGEXP, "")
|
2016-06-15 02:31:51 +08:00
|
|
|
.replace(/&(?![#\w]+;)/g, "&")
|
|
|
|
.replace(/'/g, "'")
|
|
|
|
.replace(/ \/>/g, ">");
|
|
|
|
}
|