FEATURE: add a setting to allow url schemes other than http(s)

This commit is contained in:
Neil Lalonde 2016-10-21 11:39:48 -04:00
parent 19e2eec219
commit 761cc688b4
8 changed files with 44 additions and 13 deletions

View File

@ -19,7 +19,7 @@ export function cook(text) {
} }
export function sanitize(text) { export function sanitize(text) {
return textSanitize(text, new WhiteLister(getOpts().features)); return textSanitize(text, new WhiteLister(getOpts()));
} }
function emojiOptions() { function emojiOptions() {

View File

@ -389,7 +389,7 @@ export function cook(raw, opts) {
preProcessors.forEach(p => raw = p(raw)); preProcessors.forEach(p => raw = p(raw));
const whiteLister = new WhiteLister(opts.features); const whiteLister = new WhiteLister(opts);
const tree = parser.toHTMLTree(raw, 'Discourse'); const tree = parser.toHTMLTree(raw, 'Discourse');
let result = opts.sanitizer(parser.renderJsonML(parseTree(tree, opts)), whiteLister); let result = opts.sanitizer(parser.renderJsonML(parseTree(tree, opts)), whiteLister);

View File

@ -48,6 +48,7 @@ export function buildOptions(state) {
getCurrentUser, getCurrentUser,
currentUser, currentUser,
mentionLookup: state.mentionLookup, mentionLookup: state.mentionLookup,
allowedHrefSchemes: siteSettings.allowed_href_schemes ? siteSettings.allowed_href_schemes.split('|') : null
}; };
_registerFns.forEach(fn => fn(siteSettings, options, state)); _registerFns.forEach(fn => fn(siteSettings, options, state));
@ -71,6 +72,6 @@ export default class {
} }
sanitize(html) { sanitize(html) {
return this.opts.sanitizer(html, new WhiteLister(this.opts.features)); return this.opts.sanitizer(html, new WhiteLister(this.opts));
} }
}; };

View File

@ -42,7 +42,7 @@ export function escape(string) {
return string.replace(BAD_CHARS, escapeChar); return string.replace(BAD_CHARS, escapeChar);
} }
export function hrefAllowed(href) { export function hrefAllowed(href, extraHrefMatchers) {
// escape single quotes // escape single quotes
href = href.replace(/'/g, "%27"); href = href.replace(/'/g, "%27");
@ -54,6 +54,12 @@ export function hrefAllowed(href) {
if (/^#[\w\.\-]+/i.test(href)) { return href; } if (/^#[\w\.\-]+/i.test(href)) { return href; }
// mailtos // mailtos
if (/^mailto:[\w\.\-@]+/i.test(href)) { return href; } if (/^mailto:[\w\.\-@]+/i.test(href)) { return href; }
if (extraHrefMatchers && extraHrefMatchers.length > 0) {
for (let i=0; i<extraHrefMatchers.length; i++) {
if (extraHrefMatchers[i].test(href)) { return href; }
}
}
} }
export function sanitize(text, whiteLister) { export function sanitize(text, whiteLister) {
@ -62,7 +68,13 @@ export function sanitize(text, whiteLister) {
// Allow things like <3 and <_< // Allow things like <3 and <_<
text = text.replace(/<([^A-Za-z\/\!]|$)/g, "&lt;$1"); text = text.replace(/<([^A-Za-z\/\!]|$)/g, "&lt;$1");
const whiteList = whiteLister.getWhiteList(); const whiteList = whiteLister.getWhiteList(),
allowedHrefSchemes = whiteLister.getAllowedHrefSchemes();
let extraHrefMatchers = null;
if (allowedHrefSchemes && allowedHrefSchemes.length > 0) {
extraHrefMatchers = [new RegExp('^(' + allowedHrefSchemes.join('|') + '):\/\/[\\w\\.\\-]+','i')];
}
let result = xss(text, { let result = xss(text, {
whiteList: whiteList.tagList, whiteList: whiteList.tagList,
@ -75,8 +87,8 @@ export function sanitize(text, whiteLister) {
const forAttr = forTag[name]; const forAttr = forTag[name];
if ((forAttr && (forAttr.indexOf('*') !== -1 || forAttr.indexOf(value) !== -1)) || if ((forAttr && (forAttr.indexOf('*') !== -1 || forAttr.indexOf(value) !== -1)) ||
(name.indexOf('data-') === 0 && forTag['data-*']) || (name.indexOf('data-') === 0 && forTag['data-*']) ||
((tag === 'a' && name === 'href') && hrefAllowed(value)) || ((tag === 'a' && name === 'href') && hrefAllowed(value, extraHrefMatchers)) ||
(tag === 'img' && name === 'src' && (/^data:image.*$/i.test(value) || hrefAllowed(value))) || (tag === 'img' && name === 'src' && (/^data:image.*$/i.test(value) || hrefAllowed(value, extraHrefMatchers))) ||
(tag === 'iframe' && name === 'src' && _validIframes.some(i => i.test(value)))) { (tag === 'iframe' && name === 'src' && _validIframes.some(i => i.test(value)))) {
return attr(name, value); return attr(name, value);
} }

View File

@ -13,12 +13,13 @@ function concatUniq(src, elems) {
} }
export default class WhiteLister { export default class WhiteLister {
constructor(features) { constructor(options) {
features.default = true; options.features.default = true;
this._featureKeys = Object.keys(features).filter(f => features[f]); this._featureKeys = Object.keys(options.features).filter(f => options.features[f]);
this._key = this._featureKeys.join(':'); this._key = this._featureKeys.join(':');
this._features = features; this._features = options.features;
this._options = options||{};
} }
getCustom() { getCustom() {
@ -54,6 +55,10 @@ export default class WhiteLister {
} }
return _whiteLists[this._key]; return _whiteLists[this._key];
} }
getAllowedHrefSchemes() {
return this._options.allowedHrefSchemes || [];
}
} }
// Builds our object that represents whether something is sanitized for a particular feature. // Builds our object that represents whether something is sanitized for a particular feature.

View File

@ -1332,6 +1332,7 @@ en:
embed_username_key_from_feed: "Key to pull discourse username from feed." embed_username_key_from_feed: "Key to pull discourse username from feed."
embed_title_scrubber: "Regular expression for scrubbing embeddable titles." embed_title_scrubber: "Regular expression for scrubbing embeddable titles."
embed_truncate: "Truncate the embedded posts." embed_truncate: "Truncate the embedded posts."
allowed_href_schemes: "Schemes allowed in links in addition to http and https."
embed_post_limit: "Maximum number of posts to embed." embed_post_limit: "Maximum number of posts to embed."
embed_username_required: "The username for topic creation is required." embed_username_required: "The username for topic creation is required."
embed_whitelist_selector: "CSS selector for elements that are allowed in embeds." embed_whitelist_selector: "CSS selector for elements that are allowed in embeds."

View File

@ -567,6 +567,10 @@ posting:
- code-fences - code-fences
embed_truncate: embed_truncate:
default: true default: true
allowed_href_schemes:
client: true
default: ''
type: list
email: email:
email_time_window_mins: email_time_window_mins:

View File

@ -37,9 +37,9 @@ function sanitizer(result, whiteLister) {
return result; return result;
} }
function md(input, expected, text) { function md(input, expected, text, settings) {
const opts = buildOptions({ siteSettings: {} }); const opts = buildOptions({ siteSettings: settings||{} });
opts.traditionalMarkdownLinebreaks = true; opts.traditionalMarkdownLinebreaks = true;
opts.sanitizer = sanitizer; opts.sanitizer = sanitizer;
@ -77,3 +77,11 @@ test("first", function(){
%> %>
<%= mdtest_suite %> <%= mdtest_suite %>
test("whitelisted url scheme", function() {
md("[Steam URL Scheme](steam://store/452530)", '<p><a href="steam://store/452530">Steam URL Scheme</a></p>', 'whitelists the steam url', {allowed_href_schemes: "macappstore|steam"});
});
test("forbidden url scheme", function() {
md("[Steam URL Scheme](steam://store/452530)", '<p><a>Steam URL Scheme</a></p>', 'removes the href', {allowed_href_schemes: "macappstore|itunes"});
});