diff --git a/app/assets/javascripts/pretty-text/pretty-text.js.es6 b/app/assets/javascripts/pretty-text/pretty-text.js.es6 index 094ad9a5e82..eb493b3640a 100644 --- a/app/assets/javascripts/pretty-text/pretty-text.js.es6 +++ b/app/assets/javascripts/pretty-text/pretty-text.js.es6 @@ -62,6 +62,7 @@ export function buildOptions(state) { lookupImageUrls, censoredWords, allowedHrefSchemes: siteSettings.allowed_href_schemes ? siteSettings.allowed_href_schemes.split('|') : null, + allowedIframes: (siteSettings.allowed_iframes || '').split('|'), markdownIt: true, previewing }; diff --git a/app/assets/javascripts/pretty-text/sanitizer.js.es6 b/app/assets/javascripts/pretty-text/sanitizer.js.es6 index d865c9be5ca..f62df9d4bb2 100644 --- a/app/assets/javascripts/pretty-text/sanitizer.js.es6 +++ b/app/assets/javascripts/pretty-text/sanitizer.js.es6 @@ -1,7 +1,5 @@ import xss from 'pretty-text/xss'; -const _validIframes = []; - function attr(name, value) { if (value) { return `${name}="${xss.escapeAttrValue(value)}"`; @@ -69,7 +67,8 @@ export function sanitize(text, whiteLister) { text = text.replace(/<([^A-Za-z\/\!]|$)/g, "<$1"); const whiteList = whiteLister.getWhiteList(), - allowedHrefSchemes = whiteLister.getAllowedHrefSchemes(); + allowedHrefSchemes = whiteLister.getAllowedHrefSchemes(), + allowedIframes = whiteLister.getAllowedIframes(); let extraHrefMatchers = null; if (allowedHrefSchemes && allowedHrefSchemes.length > 0) { @@ -85,11 +84,13 @@ export function sanitize(text, whiteLister) { const forTag = whiteList.attrList[tag]; if (forTag) { 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-*']) || ((tag === 'a' && name === 'href') && hrefAllowed(value, extraHrefMatchers)) || (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' && allowedIframes.some(i => { return value.toLowerCase().indexOf((i || '').toLowerCase()) === 0;})) + ) { return attr(name, value); } @@ -114,10 +115,3 @@ export function sanitize(text, whiteLister) { .replace(/'/g, "'") .replace(/ \/>/g, '>'); }; - -export function whiteListIframe(regexp) { - _validIframes.push(regexp); -} - -whiteListIframe(/^(https?:)?\/\/www\.google\.com\/maps\/embed\?.+/i); -whiteListIframe(/^(https?:)?\/\/www\.openstreetmap\.org\/export\/embed.html\?.+/i); diff --git a/app/assets/javascripts/pretty-text/white-lister.js.es6 b/app/assets/javascripts/pretty-text/white-lister.js.es6 index 30f8f23c966..cc022a827f9 100644 --- a/app/assets/javascripts/pretty-text/white-lister.js.es6 +++ b/app/assets/javascripts/pretty-text/white-lister.js.es6 @@ -9,6 +9,7 @@ export default class WhiteLister { this._enabled = { "default": true }; this._allowedHrefSchemes = (options && options.allowedHrefSchemes) || []; + this._allowedIframes = (options && options.allowedIframes) || []; this._rawFeatures = [["default", DEFAULT_LIST]]; this._cache = null; @@ -102,6 +103,10 @@ export default class WhiteLister { getAllowedHrefSchemes() { return this._allowedHrefSchemes; } + + getAllowedIframes() { + return this._allowedIframes; + } } // Only add to `default` when you always want your whitelist to occur. In other words, diff --git a/config/locales/server.en.yml b/config/locales/server.en.yml index 351832b2754..6e9a1a6a554 100644 --- a/config/locales/server.en.yml +++ b/config/locales/server.en.yml @@ -1058,6 +1058,7 @@ en: use_admin_ip_whitelist: "Admins can only log in if they are at an IP address defined in the Screened IPs list (Admin > Logs > Screened Ips)." blacklist_ip_blocks: "A list of private IP blocks that should never be crawled by Discourse" whitelist_internal_hosts: "A list of internal hosts that discourse can safely crawl for oneboxing and other purposes" + allowed_iframes: "A list of iframe src domain prefixes that discourse can safely allow in posts" top_menu: "Determine which items appear in the homepage navigation, and in what order. Example latest|new|unread|categories|top|read|posted|bookmarks" post_menu: "Determine which items appear on the post menu, and in what order. Example like|edit|flag|delete|share|bookmark|reply" post_menu_hidden_items: "The menu items to hide by default in the post menu unless an expansion ellipsis is clicked on." diff --git a/config/site_settings.yml b/config/site_settings.yml index 29e574dc73a..2cd16d920ff 100644 --- a/config/site_settings.yml +++ b/config/site_settings.yml @@ -940,6 +940,10 @@ security: whitelist_internal_hosts: default: '' type: list + allowed_iframes: + default: 'https://www.google.com/maps/embed?|https://www.openstreetmap.org/export/embed.html?' + type: list + client: true onebox: enable_flash_video_onebox: false diff --git a/spec/components/pretty_text_spec.rb b/spec/components/pretty_text_spec.rb index 4015ad3301c..0cffc4862b5 100644 --- a/spec/components/pretty_text_spec.rb +++ b/spec/components/pretty_text_spec.rb @@ -1104,4 +1104,24 @@ HTML end + it "can properly whitelist iframes" do + SiteSetting.allowed_iframes = "https://bob.com/a|http://silly.com?EMBED=" + raw = <<~IFRAMES + + + + IFRAMES + + # we require explicit HTTPS here + html = <<~IFRAMES + + + IFRAMES + + cooked = PrettyText.cook(raw).strip + + expect(cooked).to eq(html.strip) + + end + end