mirror of
https://github.com/vale981/tridactyl
synced 2025-03-06 01:51:40 -05:00
html-tagged-template.js: Accept moz-extension protocol
html-tagged-template.js forbids every protocol except http:// when sanitizing urls. This prevents Tridactyl from loading its icons from moz-extension urls. This commit fixes that (ignore prettier reformatting the file, the only line I actually changed was line 314 before prettier which became line 467 after prettier).
This commit is contained in:
parent
e62eae4d15
commit
d1ce62ec6d
1 changed files with 539 additions and 379 deletions
|
@ -1,81 +1,204 @@
|
||||||
(function(window) {
|
;(function(window) {
|
||||||
"use strict";
|
"use strict"
|
||||||
|
|
||||||
// test for es6 support of needed functionality
|
// test for es6 support of needed functionality
|
||||||
try {
|
try {
|
||||||
// spread operator and template strings support
|
// spread operator and template strings support
|
||||||
(function testSpreadOpAndTemplate() {
|
;(function testSpreadOpAndTemplate() {
|
||||||
const tag = function tag(strings, ...values) {return;};
|
const tag = function tag(strings, ...values) {
|
||||||
tag`test`;
|
return
|
||||||
})();
|
}
|
||||||
|
tag`test`
|
||||||
|
})()
|
||||||
|
|
||||||
// template tag and Array.from support
|
// template tag and Array.from support
|
||||||
if (!('content' in document.createElement('template') && 'from' in Array)) {
|
if (
|
||||||
throw new Error();
|
!(
|
||||||
|
"content" in document.createElement("template") &&
|
||||||
|
"from" in Array
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
throw new Error()
|
||||||
}
|
}
|
||||||
}
|
} catch (e) {
|
||||||
catch (e) {
|
|
||||||
// missing support;
|
// missing support;
|
||||||
console.log('Your browser does not support the needed functionality to use the html tagged template');
|
console.log(
|
||||||
return;
|
"Your browser does not support the needed functionality to use the html tagged template",
|
||||||
|
)
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if (typeof window.html === 'undefined') {
|
if (typeof window.html === "undefined") {
|
||||||
|
|
||||||
// --------------------------------------------------
|
// --------------------------------------------------
|
||||||
// constants
|
// constants
|
||||||
// --------------------------------------------------
|
// --------------------------------------------------
|
||||||
|
|
||||||
const SUBSTITUTION_INDEX = 'substitutionindex:'; // tag names are always all lowercase
|
const SUBSTITUTION_INDEX = "substitutionindex:" // tag names are always all lowercase
|
||||||
const SUBSTITUTION_REGEX = new RegExp(SUBSTITUTION_INDEX + '([0-9]+):', 'g');
|
const SUBSTITUTION_REGEX = new RegExp(
|
||||||
|
SUBSTITUTION_INDEX + "([0-9]+):",
|
||||||
|
"g",
|
||||||
|
)
|
||||||
|
|
||||||
// rejection string is used to replace xss attacks that cannot be escaped either
|
// rejection string is used to replace xss attacks that cannot be escaped either
|
||||||
// because the escaped string is still executable
|
// because the escaped string is still executable
|
||||||
// (e.g. setTimeout(/* escaped string */)) or because it produces invalid results
|
// (e.g. setTimeout(/* escaped string */)) or because it produces invalid results
|
||||||
// (e.g. <h${xss}> where xss='><script>alert(1337)</script')
|
// (e.g. <h${xss}> where xss='><script>alert(1337)</script')
|
||||||
// @see https://developers.google.com/closure/templates/docs/security#in_tags_and_attrs
|
// @see https://developers.google.com/closure/templates/docs/security#in_tags_and_attrs
|
||||||
const REJECTION_STRING = 'zXssPreventedz';
|
const REJECTION_STRING = "zXssPreventedz"
|
||||||
|
|
||||||
// which characters should be encoded in which contexts
|
// which characters should be encoded in which contexts
|
||||||
const ENCODINGS = {
|
const ENCODINGS = {
|
||||||
attribute: {
|
attribute: {
|
||||||
'&': '&',
|
"&": "&",
|
||||||
'<': '<',
|
"<": "<",
|
||||||
'>': '>'
|
">": ">",
|
||||||
},
|
},
|
||||||
uri: {
|
uri: {
|
||||||
'&': '&'
|
"&": "&",
|
||||||
|
},
|
||||||
}
|
}
|
||||||
};
|
|
||||||
|
|
||||||
// which attributes are DOM Level 0 events
|
// which attributes are DOM Level 0 events
|
||||||
// taken from https://en.wikipedia.org/wiki/DOM_events#DOM_Level_0
|
// taken from https://en.wikipedia.org/wiki/DOM_events#DOM_Level_0
|
||||||
const DOM_EVENTS = ["onclick", "ondblclick", "onmousedown", "onmouseup", "onmouseover", "onmousemove", "onmouseout", "ondragstart", "ondrag", "ondragenter", "ondragleave", "ondragover", "ondrop", "ondragend", "onkeydown", "onkeypress", "onkeyup", "onload", "onunload", "onabort", "onerror", "onresize", "onscroll", "onselect", "onchange", "onsubmit", "onreset", "onfocus", "onblur", "onpointerdown", "onpointerup", "onpointercancel", "onpointermove", "onpointerover", "onpointerout", "onpointerenter", "onpointerleave", "ongotpointercapture", "onlostpointercapture", "oncut", "oncopy", "onpaste", "onbeforecut", "onbeforecopy", "onbeforepaste", "onafterupdate", "onbeforeupdate", "oncellchange", "ondataavailable", "ondatasetchanged", "ondatasetcomplete", "onerrorupdate", "onrowenter", "onrowexit", "onrowsdelete", "onrowinserted", "oncontextmenu", "ondrag", "ondragstart", "ondragenter", "ondragover", "ondragleave", "ondragend", "ondrop", "onselectstart", "help", "onbeforeunload", "onstop", "beforeeditfocus", "onstart", "onfinish", "onbounce", "onbeforeprint", "onafterprint", "onpropertychange", "onfilterchange", "onreadystatechange", "onlosecapture", "DOMMouseScroll", "ondragdrop", "ondragenter", "ondragexit", "ondraggesture", "ondragover", "onclose", "oncommand", "oninput", "DOMMenuItemActive", "DOMMenuItemInactive", "oncontextmenu", "onoverflow", "onoverflowchanged", "onunderflow", "onpopuphidden", "onpopuphiding", "onpopupshowing", "onpopupshown", "onbroadcast", "oncommandupdate"];
|
const DOM_EVENTS = [
|
||||||
|
"onclick",
|
||||||
|
"ondblclick",
|
||||||
|
"onmousedown",
|
||||||
|
"onmouseup",
|
||||||
|
"onmouseover",
|
||||||
|
"onmousemove",
|
||||||
|
"onmouseout",
|
||||||
|
"ondragstart",
|
||||||
|
"ondrag",
|
||||||
|
"ondragenter",
|
||||||
|
"ondragleave",
|
||||||
|
"ondragover",
|
||||||
|
"ondrop",
|
||||||
|
"ondragend",
|
||||||
|
"onkeydown",
|
||||||
|
"onkeypress",
|
||||||
|
"onkeyup",
|
||||||
|
"onload",
|
||||||
|
"onunload",
|
||||||
|
"onabort",
|
||||||
|
"onerror",
|
||||||
|
"onresize",
|
||||||
|
"onscroll",
|
||||||
|
"onselect",
|
||||||
|
"onchange",
|
||||||
|
"onsubmit",
|
||||||
|
"onreset",
|
||||||
|
"onfocus",
|
||||||
|
"onblur",
|
||||||
|
"onpointerdown",
|
||||||
|
"onpointerup",
|
||||||
|
"onpointercancel",
|
||||||
|
"onpointermove",
|
||||||
|
"onpointerover",
|
||||||
|
"onpointerout",
|
||||||
|
"onpointerenter",
|
||||||
|
"onpointerleave",
|
||||||
|
"ongotpointercapture",
|
||||||
|
"onlostpointercapture",
|
||||||
|
"oncut",
|
||||||
|
"oncopy",
|
||||||
|
"onpaste",
|
||||||
|
"onbeforecut",
|
||||||
|
"onbeforecopy",
|
||||||
|
"onbeforepaste",
|
||||||
|
"onafterupdate",
|
||||||
|
"onbeforeupdate",
|
||||||
|
"oncellchange",
|
||||||
|
"ondataavailable",
|
||||||
|
"ondatasetchanged",
|
||||||
|
"ondatasetcomplete",
|
||||||
|
"onerrorupdate",
|
||||||
|
"onrowenter",
|
||||||
|
"onrowexit",
|
||||||
|
"onrowsdelete",
|
||||||
|
"onrowinserted",
|
||||||
|
"oncontextmenu",
|
||||||
|
"ondrag",
|
||||||
|
"ondragstart",
|
||||||
|
"ondragenter",
|
||||||
|
"ondragover",
|
||||||
|
"ondragleave",
|
||||||
|
"ondragend",
|
||||||
|
"ondrop",
|
||||||
|
"onselectstart",
|
||||||
|
"help",
|
||||||
|
"onbeforeunload",
|
||||||
|
"onstop",
|
||||||
|
"beforeeditfocus",
|
||||||
|
"onstart",
|
||||||
|
"onfinish",
|
||||||
|
"onbounce",
|
||||||
|
"onbeforeprint",
|
||||||
|
"onafterprint",
|
||||||
|
"onpropertychange",
|
||||||
|
"onfilterchange",
|
||||||
|
"onreadystatechange",
|
||||||
|
"onlosecapture",
|
||||||
|
"DOMMouseScroll",
|
||||||
|
"ondragdrop",
|
||||||
|
"ondragenter",
|
||||||
|
"ondragexit",
|
||||||
|
"ondraggesture",
|
||||||
|
"ondragover",
|
||||||
|
"onclose",
|
||||||
|
"oncommand",
|
||||||
|
"oninput",
|
||||||
|
"DOMMenuItemActive",
|
||||||
|
"DOMMenuItemInactive",
|
||||||
|
"oncontextmenu",
|
||||||
|
"onoverflow",
|
||||||
|
"onoverflowchanged",
|
||||||
|
"onunderflow",
|
||||||
|
"onpopuphidden",
|
||||||
|
"onpopuphiding",
|
||||||
|
"onpopupshowing",
|
||||||
|
"onpopupshown",
|
||||||
|
"onbroadcast",
|
||||||
|
"oncommandupdate",
|
||||||
|
]
|
||||||
|
|
||||||
// which attributes take URIs
|
// which attributes take URIs
|
||||||
// taken from https://www.w3.org/TR/html4/index/attributes.html
|
// taken from https://www.w3.org/TR/html4/index/attributes.html
|
||||||
const URI_ATTRIBUTES = ["action", "background", "cite", "classid", "codebase", "data", "href", "longdesc", "profile", "src", "usemap"];
|
const URI_ATTRIBUTES = [
|
||||||
|
"action",
|
||||||
|
"background",
|
||||||
|
"cite",
|
||||||
|
"classid",
|
||||||
|
"codebase",
|
||||||
|
"data",
|
||||||
|
"href",
|
||||||
|
"longdesc",
|
||||||
|
"profile",
|
||||||
|
"src",
|
||||||
|
"usemap",
|
||||||
|
]
|
||||||
|
|
||||||
const ENCODINGS_REGEX = {
|
const ENCODINGS_REGEX = {
|
||||||
attribute: new RegExp('[' + Object.keys(ENCODINGS.attribute).join('') + ']', 'g'),
|
attribute: new RegExp(
|
||||||
uri: new RegExp('[' + Object.keys(ENCODINGS.uri).join('') + ']', 'g')
|
"[" + Object.keys(ENCODINGS.attribute).join("") + "]",
|
||||||
};
|
"g",
|
||||||
|
),
|
||||||
|
uri: new RegExp(
|
||||||
|
"[" + Object.keys(ENCODINGS.uri).join("") + "]",
|
||||||
|
"g",
|
||||||
|
),
|
||||||
|
}
|
||||||
|
|
||||||
// find all attributes after the first whitespace (which would follow the tag
|
// find all attributes after the first whitespace (which would follow the tag
|
||||||
// name. Only used when the DOM has been clobbered to still parse attributes
|
// name. Only used when the DOM has been clobbered to still parse attributes
|
||||||
const ATTRIBUTE_PARSER_REGEX = /\s([^">=\s]+)(?:="[^"]+")?/g;
|
const ATTRIBUTE_PARSER_REGEX = /\s([^">=\s]+)(?:="[^"]+")?/g
|
||||||
|
|
||||||
// test if a javascript substitution is wrapped with quotes
|
// test if a javascript substitution is wrapped with quotes
|
||||||
const WRAPPED_WITH_QUOTES_REGEX = /^('|")[\s\S]*\1$/;
|
const WRAPPED_WITH_QUOTES_REGEX = /^('|")[\s\S]*\1$/
|
||||||
|
|
||||||
// allow custom attribute names that start or end with url or ui to do uri escaping
|
// allow custom attribute names that start or end with url or ui to do uri escaping
|
||||||
// @see https://developers.google.com/closure/templates/docs/security#in_urls
|
// @see https://developers.google.com/closure/templates/docs/security#in_urls
|
||||||
const CUSTOM_URI_ATTRIBUTES_REGEX = /\bur[il]|ur[il]s?$/i;
|
const CUSTOM_URI_ATTRIBUTES_REGEX = /\bur[il]|ur[il]s?$/i
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// --------------------------------------------------
|
// --------------------------------------------------
|
||||||
// private functions
|
// private functions
|
||||||
|
@ -91,8 +214,8 @@ if (typeof window.html === 'undefined') {
|
||||||
*/
|
*/
|
||||||
function encodeAttributeHTMLEntities(str) {
|
function encodeAttributeHTMLEntities(str) {
|
||||||
return str.replace(ENCODINGS_REGEX.attribute, function(match) {
|
return str.replace(ENCODINGS_REGEX.attribute, function(match) {
|
||||||
return ENCODINGS.attribute[match];
|
return ENCODINGS.attribute[match]
|
||||||
});
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -105,14 +228,10 @@ if (typeof window.html === 'undefined') {
|
||||||
*/
|
*/
|
||||||
function encodeURIEntities(str) {
|
function encodeURIEntities(str) {
|
||||||
return str.replace(ENCODINGS_REGEX.uri, function(match) {
|
return str.replace(ENCODINGS_REGEX.uri, function(match) {
|
||||||
return ENCODINGS.uri[match];
|
return ENCODINGS.uri[match]
|
||||||
});
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// --------------------------------------------------
|
// --------------------------------------------------
|
||||||
// html tagged template function
|
// html tagged template function
|
||||||
// --------------------------------------------------
|
// --------------------------------------------------
|
||||||
|
@ -129,7 +248,7 @@ if (typeof window.html === 'undefined') {
|
||||||
window.html = function(strings, ...values) {
|
window.html = function(strings, ...values) {
|
||||||
// break early if called with empty content
|
// break early if called with empty content
|
||||||
if (!strings[0] && values.length === 0) {
|
if (!strings[0] && values.length === 0) {
|
||||||
return;
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -140,56 +259,58 @@ if (typeof window.html === 'undefined') {
|
||||||
* @param {string} index - Substitution placeholder index.
|
* @param {string} index - Substitution placeholder index.
|
||||||
*/
|
*/
|
||||||
function replaceSubstitution(match, index) {
|
function replaceSubstitution(match, index) {
|
||||||
return values[parseInt(index, 10)];
|
return values[parseInt(index, 10)]
|
||||||
}
|
}
|
||||||
|
|
||||||
// insert placeholders into the generated string so we can run it through the
|
// insert placeholders into the generated string so we can run it through the
|
||||||
// HTML parser without any malicious content.
|
// HTML parser without any malicious content.
|
||||||
// (this particular placeholder will even work when used to create a DOM element)
|
// (this particular placeholder will even work when used to create a DOM element)
|
||||||
let str = strings[0];
|
let str = strings[0]
|
||||||
for (let i = 0; i < values.length; i++) {
|
for (let i = 0; i < values.length; i++) {
|
||||||
str += SUBSTITUTION_INDEX + i + ':' + strings[i+1];
|
str += SUBSTITUTION_INDEX + i + ":" + strings[i + 1]
|
||||||
}
|
}
|
||||||
|
|
||||||
// template tags allow any HTML (even <tr> elements out of context)
|
// template tags allow any HTML (even <tr> elements out of context)
|
||||||
// @see https://developer.mozilla.org/en-US/docs/Web/HTML/Element/template
|
// @see https://developer.mozilla.org/en-US/docs/Web/HTML/Element/template
|
||||||
let template = document.createElement('template');
|
let template = document.createElement("template")
|
||||||
template.innerHTML = str;
|
template.innerHTML = str
|
||||||
|
|
||||||
// find all substitution values and safely encode them using DOM APIs and
|
// find all substitution values and safely encode them using DOM APIs and
|
||||||
// contextual auto-escaping
|
// contextual auto-escaping
|
||||||
let walker = document.createNodeIterator(template.content, NodeFilter.SHOW_ALL);
|
let walker = document.createNodeIterator(
|
||||||
let node;
|
template.content,
|
||||||
while (node = walker.nextNode()) {
|
NodeFilter.SHOW_ALL,
|
||||||
let tag = null;
|
)
|
||||||
let attributesToRemove = [];
|
let node
|
||||||
|
while ((node = walker.nextNode())) {
|
||||||
|
let tag = null
|
||||||
|
let attributesToRemove = []
|
||||||
|
|
||||||
|
|
||||||
// --------------------------------------------------
|
// --------------------------------------------------
|
||||||
// node name substitution
|
// node name substitution
|
||||||
// --------------------------------------------------
|
// --------------------------------------------------
|
||||||
|
|
||||||
let nodeName = node.nodeName.toLowerCase();
|
let nodeName = node.nodeName.toLowerCase()
|
||||||
if (nodeName.indexOf(SUBSTITUTION_INDEX) !== -1) {
|
if (nodeName.indexOf(SUBSTITUTION_INDEX) !== -1) {
|
||||||
nodeName = nodeName.replace(SUBSTITUTION_REGEX, replaceSubstitution);
|
nodeName = nodeName.replace(
|
||||||
|
SUBSTITUTION_REGEX,
|
||||||
|
replaceSubstitution,
|
||||||
|
)
|
||||||
|
|
||||||
// createElement() should not need to be escaped to prevent XSS?
|
// createElement() should not need to be escaped to prevent XSS?
|
||||||
|
|
||||||
// this will throw an error if the tag name is invalid (e.g. xss tried
|
// this will throw an error if the tag name is invalid (e.g. xss tried
|
||||||
// to escape out of the tag using '><script>alert(1337)</script><')
|
// to escape out of the tag using '><script>alert(1337)</script><')
|
||||||
// instead of replacing the tag name we'll just let the error be thrown
|
// instead of replacing the tag name we'll just let the error be thrown
|
||||||
tag = document.createElement(nodeName);
|
tag = document.createElement(nodeName)
|
||||||
|
|
||||||
// mark that this node needs to be cleaned up later with the newly
|
// mark that this node needs to be cleaned up later with the newly
|
||||||
// created node
|
// created node
|
||||||
node._replacedWith = tag;
|
node._replacedWith = tag
|
||||||
|
|
||||||
// use insertBefore() instead of replaceChild() so that the node Iterator
|
// use insertBefore() instead of replaceChild() so that the node Iterator
|
||||||
// doesn't think the new tag should be the next node
|
// doesn't think the new tag should be the next node
|
||||||
node.parentNode.insertBefore(tag, node);
|
node.parentNode.insertBefore(tag, node)
|
||||||
}
|
}
|
||||||
|
|
||||||
// special case for script tags:
|
// special case for script tags:
|
||||||
|
@ -197,122 +318,155 @@ if (typeof window.html === 'undefined') {
|
||||||
// tag to not be executed when added to the DOM. We'll need to create a script
|
// tag to not be executed when added to the DOM. We'll need to create a script
|
||||||
// tag and append its contents which will make it execute correctly.
|
// tag and append its contents which will make it execute correctly.
|
||||||
// @see http://stackoverflow.com/questions/1197575/can-scripts-be-inserted-with-innerhtml
|
// @see http://stackoverflow.com/questions/1197575/can-scripts-be-inserted-with-innerhtml
|
||||||
else if (node.nodeName === 'SCRIPT') {
|
else if (node.nodeName === "SCRIPT") {
|
||||||
let script = document.createElement('script');
|
let script = document.createElement("script")
|
||||||
tag = script;
|
tag = script
|
||||||
|
|
||||||
node._replacedWith = script;
|
node._replacedWith = script
|
||||||
node.parentNode.insertBefore(script, node);
|
node.parentNode.insertBefore(script, node)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// --------------------------------------------------
|
// --------------------------------------------------
|
||||||
// attribute substitution
|
// attribute substitution
|
||||||
// --------------------------------------------------
|
// --------------------------------------------------
|
||||||
|
|
||||||
let attributes;
|
let attributes
|
||||||
if (node.attributes) {
|
if (node.attributes) {
|
||||||
|
|
||||||
// if the attributes property is not of type NamedNodeMap then the DOM
|
// if the attributes property is not of type NamedNodeMap then the DOM
|
||||||
// has been clobbered. E.g. <form><input name="attributes"></form>.
|
// has been clobbered. E.g. <form><input name="attributes"></form>.
|
||||||
// We'll manually build up an array of objects that mimic the Attr
|
// We'll manually build up an array of objects that mimic the Attr
|
||||||
// object so the loop will still work as expected.
|
// object so the loop will still work as expected.
|
||||||
if (!(node.attributes instanceof NamedNodeMap)) {
|
if (!(node.attributes instanceof NamedNodeMap)) {
|
||||||
|
|
||||||
// first clone the node so we can isolate it from any children
|
// first clone the node so we can isolate it from any children
|
||||||
let temp = node.cloneNode();
|
let temp = node.cloneNode()
|
||||||
|
|
||||||
// parse the node string for all attributes
|
// parse the node string for all attributes
|
||||||
let attributeMatches = temp.outerHTML.match(ATTRIBUTE_PARSER_REGEX);
|
let attributeMatches = temp.outerHTML.match(
|
||||||
|
ATTRIBUTE_PARSER_REGEX,
|
||||||
|
)
|
||||||
|
|
||||||
// get all attribute names and their value
|
// get all attribute names and their value
|
||||||
attributes = [];
|
attributes = []
|
||||||
for (let i = 0; i < attributeMatches.length; i++) {
|
for (let i = 0; i < attributeMatches.length; i++) {
|
||||||
let attributeName = attributeMatches[i].trim().split('=')[0];
|
let attributeName = attributeMatches[i]
|
||||||
let attributeValue = node.getAttribute(attributeName);
|
.trim()
|
||||||
|
.split("=")[0]
|
||||||
|
let attributeValue = node.getAttribute(
|
||||||
|
attributeName,
|
||||||
|
)
|
||||||
|
|
||||||
attributes.push({
|
attributes.push({
|
||||||
name: attributeName,
|
name: attributeName,
|
||||||
value: attributeValue
|
value: attributeValue,
|
||||||
});
|
})
|
||||||
}
|
}
|
||||||
}
|
} else {
|
||||||
else {
|
|
||||||
// Windows 10 Firefox 44 will shift the attributes NamedNodeMap and
|
// Windows 10 Firefox 44 will shift the attributes NamedNodeMap and
|
||||||
// push the attribute to the end when using setAttribute(). We'll have
|
// push the attribute to the end when using setAttribute(). We'll have
|
||||||
// to clone the NamedNodeMap so the order isn't changed for setAttribute()
|
// to clone the NamedNodeMap so the order isn't changed for setAttribute()
|
||||||
attributes = Array.from(node.attributes);
|
attributes = Array.from(node.attributes)
|
||||||
}
|
}
|
||||||
|
|
||||||
for (let i = 0; i < attributes.length; i++) {
|
for (let i = 0; i < attributes.length; i++) {
|
||||||
let attribute = attributes[i];
|
let attribute = attributes[i]
|
||||||
let name = attribute.name;
|
let name = attribute.name
|
||||||
let value = attribute.value;
|
let value = attribute.value
|
||||||
let hasSubstitution = false;
|
let hasSubstitution = false
|
||||||
|
|
||||||
// name has substitution
|
// name has substitution
|
||||||
if (name.indexOf(SUBSTITUTION_INDEX) !== -1) {
|
if (name.indexOf(SUBSTITUTION_INDEX) !== -1) {
|
||||||
name = name.replace(SUBSTITUTION_REGEX, replaceSubstitution);
|
name = name.replace(
|
||||||
|
SUBSTITUTION_REGEX,
|
||||||
|
replaceSubstitution,
|
||||||
|
)
|
||||||
|
|
||||||
// ensure substitution was with a non-empty string
|
// ensure substitution was with a non-empty string
|
||||||
if (name && typeof name === 'string') {
|
if (name && typeof name === "string") {
|
||||||
hasSubstitution = true;
|
hasSubstitution = true
|
||||||
}
|
}
|
||||||
|
|
||||||
// remove old attribute
|
// remove old attribute
|
||||||
attributesToRemove.push(attribute.name);
|
attributesToRemove.push(attribute.name)
|
||||||
}
|
}
|
||||||
|
|
||||||
// value has substitution - only check if name exists (only happens
|
// value has substitution - only check if name exists (only happens
|
||||||
// when name is a substitution with an empty value)
|
// when name is a substitution with an empty value)
|
||||||
if (name && value.indexOf(SUBSTITUTION_INDEX) !== -1) {
|
if (name && value.indexOf(SUBSTITUTION_INDEX) !== -1) {
|
||||||
hasSubstitution = true;
|
hasSubstitution = true
|
||||||
|
|
||||||
// if an uri attribute has been rejected
|
// if an uri attribute has been rejected
|
||||||
let isRejected = false;
|
let isRejected = false
|
||||||
|
|
||||||
value = value.replace(SUBSTITUTION_REGEX, function(match, index, offset) {
|
value = value.replace(SUBSTITUTION_REGEX, function(
|
||||||
|
match,
|
||||||
|
index,
|
||||||
|
offset,
|
||||||
|
) {
|
||||||
if (isRejected) {
|
if (isRejected) {
|
||||||
return '';
|
return ""
|
||||||
}
|
}
|
||||||
|
|
||||||
let substitutionValue = values[parseInt(index, 10)];
|
let substitutionValue =
|
||||||
|
values[parseInt(index, 10)]
|
||||||
|
|
||||||
// contextual auto-escaping:
|
// contextual auto-escaping:
|
||||||
// if attribute is a DOM Level 0 event then we need to ensure it
|
// if attribute is a DOM Level 0 event then we need to ensure it
|
||||||
// is quoted
|
// is quoted
|
||||||
if (DOM_EVENTS.indexOf(name) !== -1 &&
|
if (
|
||||||
typeof substitutionValue === 'string' &&
|
DOM_EVENTS.indexOf(name) !== -1 &&
|
||||||
!WRAPPED_WITH_QUOTES_REGEX.test(substitutionValue) ) {
|
typeof substitutionValue === "string" &&
|
||||||
substitutionValue = '"' + substitutionValue + '"';
|
!WRAPPED_WITH_QUOTES_REGEX.test(
|
||||||
|
substitutionValue,
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
substitutionValue =
|
||||||
|
'"' + substitutionValue + '"'
|
||||||
}
|
}
|
||||||
|
|
||||||
// contextual auto-escaping:
|
// contextual auto-escaping:
|
||||||
// if the attribute is a uri attribute then we need to uri encode it and
|
// if the attribute is a uri attribute then we need to uri encode it and
|
||||||
// remove bad protocols
|
// remove bad protocols
|
||||||
else if (URI_ATTRIBUTES.indexOf(name) !== -1 ||
|
else if (
|
||||||
CUSTOM_URI_ATTRIBUTES_REGEX.test(name)) {
|
URI_ATTRIBUTES.indexOf(name) !== -1 ||
|
||||||
|
CUSTOM_URI_ATTRIBUTES_REGEX.test(name)
|
||||||
|
) {
|
||||||
// percent encode if the value is inside of a query parameter
|
// percent encode if the value is inside of a query parameter
|
||||||
let queryParamIndex = value.indexOf('=');
|
let queryParamIndex = value.indexOf("=")
|
||||||
if (queryParamIndex !== -1 && offset > queryParamIndex) {
|
if (
|
||||||
substitutionValue = encodeURIComponent(substitutionValue);
|
queryParamIndex !== -1 &&
|
||||||
|
offset > queryParamIndex
|
||||||
|
) {
|
||||||
|
substitutionValue = encodeURIComponent(
|
||||||
|
substitutionValue,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// entity encode if value is part of the URL
|
// entity encode if value is part of the URL
|
||||||
else {
|
else {
|
||||||
substitutionValue = encodeURI( encodeURIEntities(substitutionValue) );
|
substitutionValue = encodeURI(
|
||||||
|
encodeURIEntities(
|
||||||
|
substitutionValue,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
// only allow the : when used after http or https otherwise reject
|
// only allow the : when used after http or https otherwise reject
|
||||||
// the entire url (will not allow any 'javascript:' or filter
|
// the entire url (will not allow any 'javascript:' or filter
|
||||||
// evasion techniques)
|
// evasion techniques)
|
||||||
if (offset === 0 && substitutionValue.indexOf(':') !== -1) {
|
if (
|
||||||
let protocol = substitutionValue.substring(0, 5);
|
offset === 0 &&
|
||||||
if (protocol.indexOf('http') === -1) {
|
substitutionValue.indexOf(":") !==
|
||||||
isRejected = true;
|
-1
|
||||||
|
) {
|
||||||
|
let protocol = substitutionValue.substring(
|
||||||
|
0,
|
||||||
|
5,
|
||||||
|
)
|
||||||
|
if (
|
||||||
|
protocol.indexOf("http") ===
|
||||||
|
-1 &&
|
||||||
|
protocol.indexOf("moz-") == -1
|
||||||
|
) {
|
||||||
|
isRejected = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -321,15 +475,19 @@ if (typeof window.html === 'undefined') {
|
||||||
// contextual auto-escaping:
|
// contextual auto-escaping:
|
||||||
// HTML encode attribute value if it is not a URL or URI to prevent
|
// HTML encode attribute value if it is not a URL or URI to prevent
|
||||||
// DOM Level 0 event handlers from executing xss code
|
// DOM Level 0 event handlers from executing xss code
|
||||||
else if (typeof substitutionValue === 'string') {
|
else if (
|
||||||
substitutionValue = encodeAttributeHTMLEntities(substitutionValue);
|
typeof substitutionValue === "string"
|
||||||
|
) {
|
||||||
|
substitutionValue = encodeAttributeHTMLEntities(
|
||||||
|
substitutionValue,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
return substitutionValue;
|
return substitutionValue
|
||||||
});
|
})
|
||||||
|
|
||||||
if (isRejected) {
|
if (isRejected) {
|
||||||
value = '#' + REJECTION_STRING;
|
value = "#" + REJECTION_STRING
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -338,19 +496,18 @@ if (typeof window.html === 'undefined') {
|
||||||
// all of that for us
|
// all of that for us
|
||||||
// @see https://www.mediawiki.org/wiki/DOM-based_XSS
|
// @see https://www.mediawiki.org/wiki/DOM-based_XSS
|
||||||
if (tag || hasSubstitution) {
|
if (tag || hasSubstitution) {
|
||||||
let el = (tag || node);
|
let el = tag || node
|
||||||
|
|
||||||
// optional attribute
|
// optional attribute
|
||||||
if (name.substr(-1) === '?') {
|
if (name.substr(-1) === "?") {
|
||||||
el.removeAttribute(name);
|
el.removeAttribute(name)
|
||||||
|
|
||||||
if (value === 'true') {
|
if (value === "true") {
|
||||||
name = name.slice(0, -1);
|
name = name.slice(0, -1)
|
||||||
el.setAttribute(name, '');
|
el.setAttribute(name, "")
|
||||||
}
|
}
|
||||||
}
|
} else {
|
||||||
else {
|
el.setAttribute(name, value)
|
||||||
el.setAttribute(name, value);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -360,49 +517,52 @@ if (typeof window.html === 'undefined') {
|
||||||
// will modify the attributes NamedNodeMap indices.
|
// will modify the attributes NamedNodeMap indices.
|
||||||
// @see https://github.com/straker/html-tagged-template/issues/13
|
// @see https://github.com/straker/html-tagged-template/issues/13
|
||||||
attributesToRemove.forEach(function(attribute) {
|
attributesToRemove.forEach(function(attribute) {
|
||||||
node.removeAttribute(attribute);
|
node.removeAttribute(attribute)
|
||||||
});
|
})
|
||||||
|
|
||||||
// append the current node to a replaced parent
|
// append the current node to a replaced parent
|
||||||
let parentNode;
|
let parentNode
|
||||||
if (node.parentNode && node.parentNode._replacedWith) {
|
if (node.parentNode && node.parentNode._replacedWith) {
|
||||||
parentNode = node.parentNode;
|
parentNode = node.parentNode
|
||||||
node.parentNode._replacedWith.appendChild(node);
|
node.parentNode._replacedWith.appendChild(node)
|
||||||
}
|
}
|
||||||
|
|
||||||
// remove the old node from the DOM
|
// remove the old node from the DOM
|
||||||
if ((node._replacedWith && node.childNodes.length === 0) ||
|
if (
|
||||||
(parentNode && parentNode.childNodes.length === 0) ){
|
(node._replacedWith && node.childNodes.length === 0) ||
|
||||||
(parentNode || node).remove();
|
(parentNode && parentNode.childNodes.length === 0)
|
||||||
|
) {
|
||||||
|
;(parentNode || node).remove()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// --------------------------------------------------
|
// --------------------------------------------------
|
||||||
// text content substitution
|
// text content substitution
|
||||||
// --------------------------------------------------
|
// --------------------------------------------------
|
||||||
|
|
||||||
if (node.nodeType === 3 && node.nodeValue.indexOf(SUBSTITUTION_INDEX) !== -1) {
|
if (
|
||||||
let nodeValue = node.nodeValue.replace(SUBSTITUTION_REGEX, replaceSubstitution);
|
node.nodeType === 3 &&
|
||||||
|
node.nodeValue.indexOf(SUBSTITUTION_INDEX) !== -1
|
||||||
|
) {
|
||||||
|
let nodeValue = node.nodeValue.replace(
|
||||||
|
SUBSTITUTION_REGEX,
|
||||||
|
replaceSubstitution,
|
||||||
|
)
|
||||||
|
|
||||||
// createTextNode() should not need to be escaped to prevent XSS?
|
// createTextNode() should not need to be escaped to prevent XSS?
|
||||||
let text = document.createTextNode(nodeValue);
|
let text = document.createTextNode(nodeValue)
|
||||||
|
|
||||||
// since the parent node has already gone through the iterator, we can use
|
// since the parent node has already gone through the iterator, we can use
|
||||||
// replaceChild() here
|
// replaceChild() here
|
||||||
node.parentNode.replaceChild(text, node);
|
node.parentNode.replaceChild(text, node)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// return the documentFragment for multiple nodes
|
// return the documentFragment for multiple nodes
|
||||||
if (template.content.childNodes.length > 1) {
|
if (template.content.childNodes.length > 1) {
|
||||||
return template.content;
|
return template.content
|
||||||
}
|
}
|
||||||
|
|
||||||
return template.content.firstChild;
|
return template.content.firstChild
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
})(window);
|
})(window)
|
||||||
|
|
Loading…
Add table
Reference in a new issue