parse_html_subset.diff

Created Diff never expires
64 removals
185 lines
129 additions
253 lines
// Copyright (c) 2012 The Chromium Authors. All rights reserved.
// Copyright (c) 2012 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
// found in the LICENSE file.


/**
/**
* Parses a very small subset of HTML. This ensures that insecure HTML /
* Parses a very small subset of HTML. This ensures that insecure HTML /
* javascript cannot be injected into WebUI.
* javascript cannot be injected into WebUI.
* @param {string} s The string to parse.
* @param {string} s The string to parse.
* @param {!Array<string>=} opt_extraTags Optional extra allowed tags.
* @param {!Array<string>=} opt_extraAttrs
* Optional extra allowed attributes (all tags are run through these).
* @throws {Error} In case of non supported markup.
* @throws {Error} In case of non supported markup.
* @return {DocumentFragment} A document fragment containing the DOM tree.
* @return {DocumentFragment} A document fragment containing the DOM tree.
*/
*/
/* #export */ const parseHtmlSubset = (function() {
/* #export */ const parseHtmlSubset = (function() {
'use strict';
'use strict';


/** @typedef {function(!Node, string):boolean} */
/** @typedef {function(!Node, string):boolean} */
let AllowFunction;
let AllowFunction;


/** @type {!AllowFunction} */
/** @type {!AllowFunction} */
const allowAttribute = (node, value) => true;
const allowAttribute = (node, value) => true;


/**
/**
* Allow-list of attributes in parseHtmlSubset.
* Allow-list of attributes in parseHtmlSubset.
* @type {!Map<string, !AllowFunction>}
* @type {!Map<string, !AllowFunction>}
* @const
* @const
*/
*/
const allowedAttributes = new Map([
const allowedAttributes = new Map([
[
[
'href',
'href',
(node, value) => {
(node, value) => {
// Only allow a[href] starting with chrome:// and https://
// Only allow a[href] starting with chrome:// and https://
return node.tagName === 'A' &&
return node.tagName === 'A' &&
(value.startsWith('chrome://') || value.startsWith('https://'));
(value.startsWith('chrome://') || value.startsWith('https://'));
}
}
],
],
[
[
'target',
'target',
(node, value) => {
(node, value) => {
// Only allow a[target='_blank'].
// Only allow a[target='_blank'].
// TODO(dbeam): are there valid use cases for target !== '_blank'?
// TODO(dbeam): are there valid use cases for target !== '_blank'?
return node.tagName === 'A' && value === '_blank';
return node.tagName === 'A' && value === '_blank';
}
}
],
],
]);

/**
* Allow-list of optional attributes in parseHtmlSubset.
* @type {!Map<string, !AllowFunction>}
* @const
*/
const allowedOptionalAttributes = new Map([
['class', allowAttribute],
['class', allowAttribute],
['id', allowAttribute],
['id', allowAttribute],
['is', (node, value) => value === 'action-link' || value === ''],
['is', (node, value) => value === 'action-link' || value === ''],
['role', (node, value) => value === 'link'],
['role', (node, value) => value === 'link'],
[
[
'src',
'src',
(node, value) => {
(node, value) => {
// Only allow img[src] starting with chrome://
// Only allow iframe[src] starting with https://
return node.tagName === 'IMG' && value.startsWith('chrome://');
return node.tagName === 'IFRAME' && value.startsWith('https://');
}
}
],
],
['tabindex', allowAttribute],
['tabindex', allowAttribute],
]);
]);


/**
/**
* Allow-list of tag names in parseHtmlSubset.
* Allow-list of tag names in parseHtmlSubset.
* @type {!Set<string>}
* @type {!Set<string>}
* @const
* @const
*/
*/

// Let's allow all HTML tags supported in DOMPurify + iframe, but except style :)
const allowedTags =
const allowedTags =
new Set(['A', 'B', 'BR', 'DIV', 'P', 'PRE', 'SPAN', 'STRONG']);
new Set([ 'A',

'ABBR',
/**
'ACRONYM',
* Allow-list of optional tag names in parseHtmlSubset.
'ADDRESS',
* @type {!Set<string>}
'AREA',
* @const
'ARTICLE',
*/
'ASIDE',
const allowedOptionalTags = new Set(['IMG']);
'AUDIO',
'B',
'BDI',
'BDO',
'BIG',
'BLINK',
'BLOCKQUOTE',
'BODY',
'BR',
'BUTTON',
'CANVAS',
'CAPTION',
'CENTER',
'CITE',
'CODE',
'COL',
'COLGROUP',
'CONTENT',
'DATA',
'DATALIST',
'DD',
'DECORATOR',
'DEL',
'DETAILS',
'DFN',
'DIALOG',
'DIR',
'DIV',
'DL',
'DT',
'ELEMENT',
'EM',
'FIELDSET',
'FIGCAPTION',
'FIGURE',
'FONT',
'FOOTER',
'FORM',
'H1',
'H2',
'H3',
'H4',
'H5',
'H6',
'HEAD',
'HEADER',
'HGROUP',
'HR',
'HTML',
'I',
'IFRAME',
'IMG',
'INPUT',
'INS',
'KBD',
'LABEL',
'LEGEND',
'LI',
'MAIN',
'MAP',
'MARK',
'MARQUEE',
'MENU',
'MENUITEM',
'METER',
'NAV',
'NOBR',
'OL',
'OPTGROUP',
'OPTION',
'OUTPUT',
'P',
'PICTURE',
'PRE',
'PROGRESS',
'Q',
'RP',
'RT',
'RUBY',
'S',
'SAMP',
'SECTION',
'SELECT',
'SHADOW',
'SMALL',
'SOURCE',
'SPACER',
'SPAN',
'STRIKE',
'STRONG',
'SUB',
'SUMMARY',
'SUP',
'TABLE',
'TBODY',
'TD',
'TEMPLATE',
'TEXTAREA',
'TFOOT',
'TH',
'THEAD',
'TIME',
'TR',
'TRACK',
'TT',
'U',
'UL',
'VAR',
'VIDEO',
'WBR']);


/**
/**
* This policy maps a given string to a `TrustedHTML` object
* This policy maps a given string to a `TrustedHTML` object
* without performing any validation. Callsites must ensure
* without performing any validation. Callsites must ensure
* that the resulting object will only be used in inert
* that the resulting object will only be used in inert
* documents. Initialized lazily.
* documents. Initialized lazily.
* @type {!TrustedTypePolicy}
* @type {!TrustedTypePolicy}
*/
*/
let unsanitizedPolicy;
let unsanitizedPolicy;


/**
* @param {!Array<string>} optTags an Array to merge.
* @return {!Set<string>} Set of allowed tags.
*/
function mergeTags(optTags) {
const clone = new Set(allowedTags);
optTags.forEach(str => {
const tag = str.toUpperCase();
if (allowedOptionalTags.has(tag)) {
clone.add(tag);
}
});
return clone;
}

/**
* @param {!Array<string>} optAttrs an Array to merge.
* @return {!Map<string, !AllowFunction>} Map of allowed
* attributes.
*/
function mergeAttrs(optAttrs) {
const clone = new Map([...allowedAttributes]);
optAttrs.forEach(key => {
if (allowedOptionalAttributes.has(key)) {
clone.set(key, allowedOptionalAttributes.get(key));
}
});
return clone;
}

function walk(n, f) {
function walk(n, f) {
f(n);
f(n);
for (let i = 0; i < n.childNodes.length; i++) {
for (let i = 0; i < n.childNodes.length; i++) {
walk(n.childNodes[i], f);
walk(n.childNodes[i], f);
}
}
}
}


function assertElement(tags, node) {
function assertElement(tags, node) {
if (!tags.has(node.tagName)) {
if (!tags.has(node.tagName)) {
throw Error(node.tagName + ' is not supported');
throw Error(node.tagName + ' is not supported');
}
}
}
}


function assertAttribute(attrs, attrNode, node) {
function assertAttribute(attrs, attrNode, node) {
const n = attrNode.nodeName;
const n = attrNode.nodeName;
const v = attrNode.nodeValue;
const v = attrNode.nodeValue;
if (!attrs.has(n) || !attrs.get(n)(node, v)) {
if (!attrs.has(n) || !attrs.get(n)(node, v)) {
throw Error(node.tagName + '[' + n + '="' + v + '"] is not supported');
throw Error(node.tagName + '[' + n + '="' + v + '"] is not supported');
}
}
}
}


return function(s, opt_extraTags, opt_extraAttrs) {
return function(s) {
const tags = opt_extraTags ? mergeTags(opt_extraTags) : allowedTags;
const tags = allowedTags;
const attrs =
const attrs = allowedAttributes;
opt_extraAttrs ? mergeAttrs(opt_extraAttrs) : allowedAttributes;


const doc = document.implementation.createHTMLDocument('');
const doc = document.implementation.createHTMLDocument('');
const r = doc.createRange();
const r = doc.createRange();
r.selectNode(doc.body);
r.selectNode(doc.body);


if (window.trustedTypes) {
if (window.trustedTypes) {
if (!unsanitizedPolicy) {
if (!unsanitizedPolicy) {
unsanitizedPolicy = trustedTypes.createPolicy(
unsanitizedPolicy = trustedTypes.createPolicy(
'parse-html-subset', {createHTML: untrustedHTML => untrustedHTML});
'parse-html-subset', {createHTML: untrustedHTML => untrustedHTML});
}
}
s = unsanitizedPolicy.createHTML(s);
s = unsanitizedPolicy.createHTML(s);
}
}


// This does not execute any scripts because the document has no view.
// This does not execute any scripts because the document has no view.
const df = r.createContextualFragment(s);
const df = r.createContextualFragment(s);
walk(df, function(node) {
walk(df, function(node) {
switch (node.nodeType) {
switch (node.nodeType) {
case Node.ELEMENT_NODE:
case Node.ELEMENT_NODE:
assertElement(tags, node);
assertElement(tags, node);
const nodeAttrs = node.attributes;
const nodeAttrs = node.attributes;
for (let i = 0; i < nodeAttrs.length; ++i) {
for (let i = 0; i < nodeAttrs.length; ++i) {
assertAttribute(attrs, nodeAttrs[i], node);
assertAttribute(attrs, nodeAttrs[i], node);
}
}
break;
break;


case Node.COMMENT_NODE:
case Node.COMMENT_NODE:
case Node.DOCUMENT_FRAGMENT_NODE:
case Node.DOCUMENT_FRAGMENT_NODE:
case Node.TEXT_NODE:
case Node.TEXT_NODE:
break;
break;


default:
default:
throw Error('Node type ' + node.nodeType + ' is not supported');
throw Error('Node type ' + node.nodeType + ' is not supported');
}
}
});
});
return df;
return df;
};
};
})();
})();


/* #ignore */ console.warn('crbug/1173575, non-JS module files deprecated.');