FIX: pretty text allow list (#10977)

Reword whitelist to allowlist in pretty-text.
This library is used by plugins so we need deprecation notice.
This commit is contained in:
Krzysztof Kotlarek 2020-10-28 13:22:06 +11:00 committed by GitHub
parent 632942e697
commit dbec3792b7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
21 changed files with 315 additions and 272 deletions

View File

@ -268,7 +268,7 @@ export default Controller.extend(ModalFunctionality, {
} else {
const opts = {
features: { editHistory: true, historyOneboxes: true },
whiteListed: {
allowListed: {
editHistory: { custom: (tag, attr) => attr === "class" },
historyOneboxes: ["header", "article", "div[style]"],
},

View File

@ -1,7 +1,7 @@
import { getURLWithCDN } from "discourse-common/lib/get-url";
import PrettyText, { buildOptions } from "pretty-text/pretty-text";
import { performEmojiUnescape, buildEmojiUrl } from "pretty-text/emoji";
import WhiteLister from "pretty-text/white-lister";
import AllowLister from "pretty-text/allow-lister";
import { sanitize as textSanitize } from "pretty-text/sanitizer";
import loadScript from "discourse/lib/load-script";
import { formatUsername } from "discourse/lib/utilities";
@ -49,7 +49,7 @@ export function generateCookFunction(options) {
}
export function sanitize(text, options) {
return textSanitize(text, new WhiteLister(options));
return textSanitize(text, new AllowLister(options));
}
export function sanitizeAsync(text, options) {

View File

@ -1,36 +1,36 @@
import { test, module } from "qunit";
import WhiteLister from "pretty-text/white-lister";
import AllowLister from "pretty-text/allow-lister";
module("lib:whiteLister");
module("lib:allowLister");
test("whiteLister", (assert) => {
const whiteLister = new WhiteLister();
test("allowLister", (assert) => {
const allowLister = new AllowLister();
assert.ok(
Object.keys(whiteLister.getWhiteList().tagList).length > 1,
Object.keys(allowLister.getAllowList().tagList).length > 1,
"should have some defaults"
);
whiteLister.disable("default");
allowLister.disable("default");
assert.ok(
Object.keys(whiteLister.getWhiteList().tagList).length === 0,
Object.keys(allowLister.getAllowList().tagList).length === 0,
"should have no defaults if disabled"
);
whiteLister.whiteListFeature("test", [
allowLister.allowListFeature("test", [
"custom.foo",
"custom.baz",
"custom[data-*]",
"custom[rel=nofollow]",
]);
whiteLister.whiteListFeature("test", ["custom[rel=test]"]);
allowLister.allowListFeature("test", ["custom[rel=test]"]);
whiteLister.enable("test");
allowLister.enable("test");
assert.deepEqual(
whiteLister.getWhiteList(),
allowLister.getAllowList(),
{
tagList: {
custom: [],
@ -46,10 +46,10 @@ test("whiteLister", (assert) => {
"Expecting a correct white list"
);
whiteLister.disable("test");
allowLister.disable("test");
assert.deepEqual(
whiteLister.getWhiteList(),
allowLister.getAllowList(),
{
tagList: {},
attrList: {},

View File

@ -6,6 +6,7 @@
//= require ./pretty-text/addon/emoji
//= require ./pretty-text/addon/engines/discourse-markdown-it
//= require xss.min
//= require ./pretty-text/addon/allow-lister
//= require ./pretty-text/addon/white-lister
//= require ./pretty-text/addon/sanitizer
//= require ./pretty-text/addon/oneboxer

View File

@ -0,0 +1,246 @@
import deprecated from "discourse-common/lib/deprecated";
// to match:
// abcd
// abcd[test]
// abcd[test=bob]
const ALLOWLIST_REGEX = /([^\[]+)(\[([^=]+)(=(.*))?\])?/;
export default class AllowLister {
constructor(options) {
this._enabled = { default: true };
this._allowedHrefSchemes = (options && options.allowedHrefSchemes) || [];
this._allowedIframes = (options && options.allowedIframes) || [];
this._rawFeatures = [["default", DEFAULT_LIST]];
this._cache = null;
if (options && options.features) {
Object.keys(options.features).forEach((f) => {
if (options.features[f]) {
this._enabled[f] = true;
}
});
}
}
allowListFeature(feature, info) {
this._rawFeatures.push([feature, info]);
}
whiteListFeature(feature, info) {
deprecated("`whiteListFeature` has been replaced with `allowListFeature`", {
since: "2.6.0.beta.4",
dropFrom: "2.7.0",
});
this.allowListFeature(feature, info);
}
disable(feature) {
this._enabled[feature] = false;
this._cache = null;
}
enable(feature) {
this._enabled[feature] = true;
this._cache = null;
}
_buildCache() {
const tagList = {};
const attrList = {};
const custom = [];
this._rawFeatures.forEach(([name, info]) => {
if (!this._enabled[name]) {
return;
}
if (info.custom) {
custom.push(info.custom);
return;
}
if (typeof info === "string") {
info = [info];
}
(info || []).forEach((tag) => {
const classes = tag.split(".");
const tagWithAttr = classes.shift();
const m = ALLOWLIST_REGEX.exec(tagWithAttr);
if (m) {
const [, tagname, , attr, , val] = m;
tagList[tagname] = [];
let attrs = (attrList[tagname] = attrList[tagname] || {});
if (classes.length > 0) {
attrs["class"] = (attrs["class"] || []).concat(classes);
}
if (attr) {
let attrInfo = (attrs[attr] = attrs[attr] || []);
if (val) {
attrInfo.push(val);
} else {
attrs[attr] = ["*"];
}
}
}
});
});
this._cache = { custom, allowList: { tagList, attrList } };
}
_ensureCache() {
if (!this._cache) {
this._buildCache();
}
}
getAllowList() {
this._ensureCache();
return this._cache.allowList;
}
getWhiteList() {
deprecated("`getWhiteList` has been replaced with `getAllowList`", {
since: "2.6.0.beta.4",
dropFrom: "2.7.0",
});
return this.getAllowList();
}
getCustom() {
this._ensureCache();
return this._cache.custom;
}
getAllowedHrefSchemes() {
return this._allowedHrefSchemes;
}
getAllowedIframes() {
return this._allowedIframes;
}
}
// Only add to `default` when you always want your allowlist to occur. In other words,
// don't change this for a plugin or a feature that can be disabled
export const DEFAULT_LIST = [
"a.attachment",
"a.hashtag",
"a.mention",
"a.mention-group",
"a.onebox",
`a.inline-onebox`,
`a.inline-onebox-loading`,
"a[data-bbcode]",
"a[name]",
"a[rel=nofollow]",
"a[rel=ugc]",
"a[target=_blank]",
"a[title]",
"abbr[title]",
"aside.quote",
"aside[data-*]",
"audio",
"audio[controls]",
"audio[preload]",
"b",
"big",
"blockquote",
"br",
"code",
"dd",
"del",
"div",
"div.quote-controls",
"div.title",
"div[align]",
"div[lang]",
"div[data-*]" /* This may seem a bit much but polls does
it anyway and this is needed for themes,
special code in sanitizer handles data-*
nothing exists for data-theme-* and we
don't want to slow sanitize for this case
*/,
"div[dir]",
"dl",
"dt",
"em",
"h1",
"h2",
"h3",
"h4",
"h5",
"h6",
"hr",
"i",
"iframe",
"iframe[frameborder]",
"iframe[height]",
"iframe[marginheight]",
"iframe[marginwidth]",
"iframe[width]",
"iframe[allowfullscreen]",
"img[alt]",
"img[height]",
"img[title]",
"img[width]",
"ins",
"kbd",
"li",
"ol",
"ol[start]",
"p",
"p[lang]",
"picture",
"pre",
"s",
"small",
"span[lang]",
"span.excerpt",
"div.excerpt",
"div.video-container",
"div.onebox-placeholder-container",
"span.placeholder-icon video",
"span.hashtag",
"span.mention",
"strike",
"strong",
"sub",
"sup",
"source[data-orig-src]",
"source[src]",
"source[srcset]",
"source[type]",
"track",
"track[default]",
"track[label]",
"track[kind]",
"track[src]",
"track[srclang]",
"ul",
"video",
"video[autoplay]",
"video[controls]",
"video[controlslist]",
"video[crossorigin]",
"video[height]",
"video[loop]",
"video[muted]",
"video[playsinline]",
"video[poster]",
"video[preload]",
"video[width]",
"ruby",
"ruby[lang]",
"rb",
"rb[lang]",
"rp",
"rt",
"rt[lang]",
];

View File

@ -1,6 +1,7 @@
import WhiteLister from "pretty-text/white-lister";
import AllowLister from "pretty-text/allow-lister";
import { sanitize } from "pretty-text/sanitizer";
import guid from "pretty-text/guid";
import deprecated from "discourse-common/lib/deprecated";
export const ATTACHMENT_CSS_CLASS = "attachment";
@ -23,11 +24,19 @@ function createHelper(
optionCallbacks,
pluginCallbacks,
getOptions,
whiteListed
allowListed
) {
let helper = {};
helper.markdownIt = true;
helper.whiteList = (info) => whiteListed.push([featureName, info]);
helper.allowList = (info) => allowListed.push([featureName, info]);
helper.whiteList = (info) => {
deprecated("`whiteList` has been replaced with `allowList`", {
since: "2.6.0.beta.4",
dropFrom: "2.7.0",
});
helper.allowList(info);
};
helper.registerInline = deprecate(featureName, "registerInline");
helper.replaceBlock = deprecate(featureName, "replaceBlock");
helper.addPreProcessor = deprecate(featureName, "addPreProcessor");
@ -296,7 +305,7 @@ export function setup(opts, siteSettings, state) {
const check = /discourse-markdown\/|markdown-it\//;
let features = [];
let whiteListed = [];
let allowListed = [];
Object.keys(require._eak_seen).forEach((entry) => {
if (check.test(entry)) {
@ -319,13 +328,13 @@ export function setup(opts, siteSettings, state) {
optionCallbacks,
pluginCallbacks,
getOptions,
whiteListed
allowListed
)
);
});
Object.entries(state.whiteListed || {}).forEach((entry) => {
whiteListed.push(entry);
Object.entries(state.allowListed || {}).forEach((entry) => {
allowListed.push(entry);
});
optionCallbacks.forEach(([, callback]) => {
@ -393,14 +402,14 @@ export function setup(opts, siteSettings, state) {
opts.setup = true;
if (!opts.discourse.sanitizer || !opts.sanitizer) {
const whiteLister = new WhiteLister(opts.discourse);
const allowLister = new AllowLister(opts.discourse);
whiteListed.forEach(([feature, info]) => {
whiteLister.whiteListFeature(feature, info);
allowListed.forEach(([feature, info]) => {
allowLister.allowListFeature(feature, info);
});
opts.sanitizer = opts.discourse.sanitizer = !!opts.discourse.sanitize
? (a) => sanitize(a, whiteLister)
? (a) => sanitize(a, allowLister)
: (a) => a;
}
}

View File

@ -71,7 +71,7 @@ export function hrefAllowed(href, extraHrefMatchers) {
}
}
export function sanitize(text, whiteLister) {
export function sanitize(text, allowLister) {
if (!text) {
return "";
}
@ -79,9 +79,9 @@ export function sanitize(text, whiteLister) {
// Allow things like <3 and <_<
text = text.replace(/<([^A-Za-z\/\!]|$)/g, "&lt;$1");
const whiteList = whiteLister.getWhiteList(),
allowedHrefSchemes = whiteLister.getAllowedHrefSchemes(),
allowedIframes = whiteLister.getAllowedIframes();
const allowList = allowLister.getAllowList(),
allowedHrefSchemes = allowLister.getAllowedHrefSchemes(),
allowedIframes = allowLister.getAllowedIframes();
let extraHrefMatchers = null;
if (allowedHrefSchemes && allowedHrefSchemes.length > 0) {
@ -94,12 +94,12 @@ export function sanitize(text, whiteLister) {
}
let result = xss(text, {
whiteList: whiteList.tagList,
whiteList: allowList.tagList,
stripIgnoreTag: true,
stripIgnoreTagBody: ["script", "table"],
onIgnoreTagAttr(tag, name, value) {
const forTag = whiteList.attrList[tag];
const forTag = allowList.attrList[tag];
if (forTag) {
const forAttr = forTag[name];
if (
@ -134,7 +134,7 @@ export function sanitize(text, whiteLister) {
return attr(name, value);
}
const custom = whiteLister.getCustom();
const custom = allowLister.getCustom();
for (let i = 0; i < custom.length; i++) {
const fn = custom[i];
if (fn(tag, name, value)) {

View File

@ -1,229 +1,15 @@
// to match:
// abcd
// abcd[test]
// abcd[test=bob]
const WHITELIST_REGEX = /([^\[]+)(\[([^=]+)(=(.*))?\])?/;
import deprecated from "discourse-common/lib/deprecated";
import AllowLister from "pretty-text/allow-lister";
import { DEFAULT_LIST as NEW_DEFAULT_LIST } from "pretty-text/allow-lister";
export default class WhiteLister {
export default class WhiteLister extends AllowLister {
constructor(options) {
this._enabled = { default: true };
this._allowedHrefSchemes = (options && options.allowedHrefSchemes) || [];
this._allowedIframes = (options && options.allowedIframes) || [];
this._rawFeatures = [["default", DEFAULT_LIST]];
this._cache = null;
if (options && options.features) {
Object.keys(options.features).forEach((f) => {
if (options.features[f]) {
this._enabled[f] = true;
}
});
}
}
whiteListFeature(feature, info) {
this._rawFeatures.push([feature, info]);
}
disable(feature) {
this._enabled[feature] = false;
this._cache = null;
}
enable(feature) {
this._enabled[feature] = true;
this._cache = null;
}
_buildCache() {
const tagList = {};
const attrList = {};
const custom = [];
this._rawFeatures.forEach(([name, info]) => {
if (!this._enabled[name]) {
return;
}
if (info.custom) {
custom.push(info.custom);
return;
}
if (typeof info === "string") {
info = [info];
}
(info || []).forEach((tag) => {
const classes = tag.split(".");
const tagWithAttr = classes.shift();
const m = WHITELIST_REGEX.exec(tagWithAttr);
if (m) {
const [, tagname, , attr, , val] = m;
tagList[tagname] = [];
let attrs = (attrList[tagname] = attrList[tagname] || {});
if (classes.length > 0) {
attrs["class"] = (attrs["class"] || []).concat(classes);
}
if (attr) {
let attrInfo = (attrs[attr] = attrs[attr] || []);
if (val) {
attrInfo.push(val);
} else {
attrs[attr] = ["*"];
}
}
}
});
deprecated("`WhiteLister` has been replaced with `AllowLister`", {
since: "2.6.0.beta.4",
dropFrom: "2.7.0",
});
this._cache = { custom, whiteList: { tagList, attrList } };
}
_ensureCache() {
if (!this._cache) {
this._buildCache();
}
}
getWhiteList() {
this._ensureCache();
return this._cache.whiteList;
}
getCustom() {
this._ensureCache();
return this._cache.custom;
}
getAllowedHrefSchemes() {
return this._allowedHrefSchemes;
}
getAllowedIframes() {
return this._allowedIframes;
super(options);
}
}
// Only add to `default` when you always want your allowlist to occur. In other words,
// don't change this for a plugin or a feature that can be disabled
export const DEFAULT_LIST = [
"a.attachment",
"a.hashtag",
"a.mention",
"a.mention-group",
"a.onebox",
`a.inline-onebox`,
`a.inline-onebox-loading`,
"a[data-bbcode]",
"a[name]",
"a[rel=nofollow]",
"a[rel=ugc]",
"a[target=_blank]",
"a[title]",
"abbr[title]",
"aside.quote",
"aside[data-*]",
"audio",
"audio[controls]",
"audio[preload]",
"b",
"big",
"blockquote",
"br",
"code",
"dd",
"del",
"div",
"div.quote-controls",
"div.title",
"div[align]",
"div[lang]",
"div[data-*]" /* This may seem a bit much but polls does
it anyway and this is needed for themes,
special code in sanitizer handles data-*
nothing exists for data-theme-* and we
don't want to slow sanitize for this case
*/,
"div[dir]",
"dl",
"dt",
"em",
"h1",
"h2",
"h3",
"h4",
"h5",
"h6",
"hr",
"i",
"iframe",
"iframe[frameborder]",
"iframe[height]",
"iframe[marginheight]",
"iframe[marginwidth]",
"iframe[width]",
"iframe[allowfullscreen]",
"img[alt]",
"img[height]",
"img[title]",
"img[width]",
"ins",
"kbd",
"li",
"ol",
"ol[start]",
"p",
"p[lang]",
"picture",
"pre",
"s",
"small",
"span[lang]",
"span.excerpt",
"div.excerpt",
"div.video-container",
"div.onebox-placeholder-container",
"span.placeholder-icon video",
"span.hashtag",
"span.mention",
"strike",
"strong",
"sub",
"sup",
"source[data-orig-src]",
"source[src]",
"source[srcset]",
"source[type]",
"track",
"track[default]",
"track[label]",
"track[kind]",
"track[src]",
"track[srclang]",
"ul",
"video",
"video[autoplay]",
"video[controls]",
"video[controlslist]",
"video[crossorigin]",
"video[height]",
"video[loop]",
"video[muted]",
"video[playsinline]",
"video[poster]",
"video[preload]",
"video[width]",
"ruby",
"ruby[lang]",
"rb",
"rb[lang]",
"rp",
"rt",
"rt[lang]",
];
export const DEFAULT_LIST = NEW_DEFAULT_LIST;

View File

@ -147,7 +147,7 @@ function processBBCode(state, silent) {
}
export function setup(helper) {
helper.whiteList([
helper.allowList([
"span.bbcode-b",
"span.bbcode-i",
"span.bbcode-u",

View File

@ -41,7 +41,7 @@ export function setup(helper) {
.concat(["auto", "nohighlight"]);
});
helper.whiteList({
helper.allowList({
custom(tag, name, value) {
if (tag === "code" && name === "class") {
const m = /^lang\-(.+)$/.exec(value);

View File

@ -68,5 +68,5 @@ export function setup(helper) {
md.block.bbcode.ruler.push("block-wrap", blockRule);
});
helper.whiteList([`div.${WRAP_CLASS}`, `span.${WRAP_CLASS}`, "span[data-*]"]);
helper.allowList([`div.${WRAP_CLASS}`, `span.${WRAP_CLASS}`, "span[data-*]"]);
}

View File

@ -339,7 +339,7 @@ export function setup(helper) {
);
});
helper.whiteList([
helper.allowList([
"img[class=emoji]",
"img[class=emoji emoji-custom]",
"img[class=emoji emoji-custom only-emoji]",

View File

@ -2,7 +2,7 @@ export function setup(helper) {
const opts = helper.getOptions();
if (opts.previewing && opts.injectLineNumbersToPreview) {
helper.whiteList([
helper.allowList([
"p.preview-sync-line",
"p[data-line-number]",
"h1.preview-sync-line",

View File

@ -168,8 +168,8 @@ export function setup(helper) {
md.block.bbcode.ruler.push("quotes", rule);
});
helper.whiteList(["img[class=avatar]"]);
helper.whiteList({
helper.allowList(["img[class=avatar]"]);
helper.allowList({
custom(tag, name, value) {
if (tag === "aside" && name === "class") {
return (

View File

@ -71,7 +71,7 @@ export const priority = 1;
export function setup(helper) {
const opts = helper.getOptions();
if (opts.previewing) {
helper.whiteList([
helper.allowList([
"span.image-wrapper",
"span.button-wrapper",
"span[class=scale-btn]",

View File

@ -10,7 +10,7 @@ export function setup(helper) {
});
// we need a custom callback for style handling
helper.whiteList({
helper.allowList({
custom: function (tag, attr, val) {
if (tag !== "th" && tag !== "td") {
return false;
@ -28,7 +28,7 @@ export function setup(helper) {
},
});
helper.whiteList([
helper.allowList([
"table",
"tbody",
"thead",

View File

@ -91,10 +91,10 @@ function rule(state) {
export function setup(helper) {
const opts = helper.getOptions();
if (opts.previewing) {
helper.whiteList(["img.resizable"]);
helper.allowList(["img.resizable"]);
}
helper.whiteList([
helper.allowList([
"img[data-orig-src]",
"img[data-base62-sha1]",
"a[data-orig-href]",

View File

@ -90,6 +90,7 @@ module PrettyText
apply_es6_file(ctx, root_path, "discourse-common/addon/lib/get-url")
apply_es6_file(ctx, root_path, "discourse-common/addon/lib/object")
apply_es6_file(ctx, root_path, "discourse-common/addon/lib/deprecated")
apply_es6_file(ctx, root_path, "discourse/app/lib/to-markdown")
apply_es6_file(ctx, root_path, "discourse/app/lib/utilities")

View File

@ -17,7 +17,7 @@ const rule = {
};
export function setup(helper) {
helper.whiteList([
helper.allowList([
"summary",
"summary[title]",
"details",

View File

@ -138,7 +138,7 @@ function closeBuffer(buffer, state, text) {
}
export function setup(helper) {
helper.whiteList([
helper.allowList([
"span.discourse-local-date",
"span[data-*]",
"span[aria-label]",

View File

@ -272,7 +272,7 @@ function newApiInit(helper) {
}
export function setup(helper) {
helper.whiteList([
helper.allowList([
"div.poll",
"div.poll-info",
"div.poll-container",