Huge News!Announcing our $40M Series B led by Abstract Ventures.Learn More
Socket
Sign inDemoInstall
Socket

html-tagged-template

Package Overview
Dependencies
Maintainers
1
Versions
6
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

html-tagged-template - npm Package Compare versions

Comparing version 1.0.0 to 2.0.0

index.html

7

bower.json

@@ -21,3 +21,6 @@ {

},
"main": "index.js",
"main": [
"index.js",
"index.html"
],
"moduleType": "globals",

@@ -32,2 +35,2 @@ "license": "MIT",

]
}
}

@@ -17,2 +17,3 @@ (function(window) {

console.log('Your browser does not support the needed functionality to use the html tagged template');
return;
}

@@ -22,5 +23,105 @@

var substitutionIndex = 'substitutionindex:'; // tag names are always all lowercase
var substitutionRegex = new RegExp(substitutionIndex + '([0-9]+):', 'g');
// --------------------------------------------------
// constants
// --------------------------------------------------
const SUBSTITUION_INDEX = 'substitutionindex:'; // tag names are always all lowercase
const SUBSTITUTION_REGEX = new RegExp(SUBSTITUION_INDEX + '([0-9]+):', 'g');
// rejection string is used to replace xss attacks that cannot be escaped either
// because the escaped string is still executable
// (e.g. setTimeout(/* escaped string */)) or because it produces invalid results
// (e.g. <h${xss}> where xss='><script>alert(1337)</script')
// @see https://developers.google.com/closure/templates/docs/security#in_tags_and_attrs
const REJECTION_STRING = 'zXssPreventedz';
// which characters should be encoded in which contexts
const ENCODINGS = {
attribute: {
'&': '&amp;',
'<': '&lt;',
'>': '&gt;'
},
uri: {
'&': '&amp;'
}
};
// which attributes are DOM Level 0 events
// 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"];
// which attributes take URIs
// 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 ENCODINGS_REGEX = {
attribute: new RegExp('[' + 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
// name. Only used when the DOM has been clobbered to still parse attributes
const ATTRIBUTE_PARSER_REGEX = /\s([^">=\s]+)(?:="[^"]+")?/g;
// test if a javascript substitution is wrapped with quotes
const WRAPPED_WITH_QUOTES_REGEX = /^('|")[\s\S]*\1$/;
// 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
const CUSTOM_URI_ATTRIBUTES_REGEX = /\bur[il]|ur[il]s?$/i;
// --------------------------------------------------
// private functions
// --------------------------------------------------
/**
* Escape HTML entities in an attribute.
* @private
*
* @param {string} str - String to escape.
*
* @returns {string}
*/
function encodeAttributeHTMLEntities(str) {
return str.replace(ENCODINGS_REGEX.attribute, function(match) {
return ENCODINGS.attribute[match];
});
}
/**
* Escape entities in a URI.
* @private
*
* @param {string} str - URI to escape.
*
* @returns {string}
*/
function encodeURIEntities(str) {
return str.replace(ENCODINGS_REGEX.uri, function(match) {
return ENCODINGS.uri[match];
});
}
// --------------------------------------------------
// html tagged template function
// --------------------------------------------------
/**
* Safely convert a DOM string into DOM nodes using by using E4H and contextual
* auto-escaping techniques to prevent xss attacks.
*
* @param {string[]} strings - Safe string literals.
* @param {*} values - Unsafe substitution expressions.
*
* @returns {HTMLElement|DocumentFragment}
*/
window.html = function(strings, ...values) {

@@ -33,3 +134,7 @@ // break early if called with empty content

/**
* Replace a string with substitutions with their substitution value
* Replace a string with substitution placeholders with its substitution values.
* @private
*
* @param {string} match - Matched substitution placeholder.
* @param {string} index - Substitution placeholder index.
*/

@@ -43,5 +148,5 @@ function replaceSubstitution(match, index) {

// (this particular placeholder will even work when used to create a DOM element)
var str = strings[0];
for (i = 0; i < values.length; i++) {
str += substitutionIndex + i + ':' + strings[i+1];
let str = strings[0];
for (let i = 0; i < values.length; i++) {
str += SUBSTITUION_INDEX + i + ':' + strings[i+1];
}

@@ -51,18 +156,34 @@

// @see https://developer.mozilla.org/en-US/docs/Web/HTML/Element/template
var template = document.createElement('template');
let template = document.createElement('template');
template.innerHTML = str;
// find all substitution values and safely encode them using DOM APIs
var walker = document.createNodeIterator(template.content, NodeFilter.SHOW_ALL);
var node;
// find all substitution values and safely encode them using DOM APIs and
// contextual auto-escaping
let walker = document.createNodeIterator(template.content, NodeFilter.SHOW_ALL);
let node;
while (node = walker.nextNode()) {
var tag = null;
let tag = null;
let attributesToRemove = [];
// node name
var nodeName = node.nodeName.toLowerCase();
if (nodeName.indexOf(substitutionIndex) !== -1) {
nodeName = nodeName.replace(substitutionRegex, replaceSubstitution);
// --------------------------------------------------
// node name substitution
// --------------------------------------------------
let nodeName = node.nodeName.toLowerCase();
if (nodeName.indexOf(SUBSTITUION_INDEX) !== -1) {
nodeName = nodeName.replace(SUBSTITUTION_REGEX, replaceSubstitution);
// 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
// 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
tag = document.createElement(nodeName);
// mark that this node needs to be cleaned up later with the newly
// created node
node._replacedWith = tag;

@@ -80,46 +201,135 @@

// @see http://stackoverflow.com/questions/1197575/can-scripts-be-inserted-with-innerhtml
if ((tag || node).nodeName === 'SCRIPT') {
var script = document.createElement('script');
else if (node.nodeName === 'SCRIPT') {
let script = document.createElement('script');
tag = script;
node._replacedWith = script;
node.parentNode.insertBefore(script, node);
}
(tag || node).parentNode.insertBefore(script, (tag || node));
tag = script;
}
// node attributes
var attributes;
if (attributes = node.attributes) {
for (var i = 0; i < attributes.length; i++) {
var attribute = attributes[i];
var name = attribute.name;
var value = attribute.value;
var hasSubstitution = false;
// --------------------------------------------------
// attribute substitution
// --------------------------------------------------
let attributes;
if (node.attributes) {
// if the attributes property is not of type NamedNodeMap then the DOM
// has been clobbered. E.g. <form><input name="attributes"></form>.
// We'll manually build up an array of objects that mimic the Attr
// object so the loop will still work as expected.
if ( !(node.attributes instanceof NamedNodeMap) ) {
// first clone the node so we can isolate it from any children
let temp = node.cloneNode();
// parse the node string for all attributes
let attributeMatches = temp.outerHTML.match(ATTRIBUTE_PARSER_REGEX);
// get all attribute names and their value
attributes = [];
for (let i = 0; i < attributeMatches.length; i++) {
let attributeName = attributeMatches[i].trim().split('=')[0];
let attributeValue = node.getAttribute(attributeName);
attributes.push({
name: attributeName,
value: attributeValue
});
}
}
else {
// Windows 10 Firefox 44 will shift the attributes NamedNodeMap and
// push the attribute to the end when using setAttribute(). We'll have
// to clone the NamedNodeMap so the order isn't changed for setAttribute()
attributes = Array.from(node.attributes);
}
for (let i = 0; i < attributes.length; i++) {
let attribute = attributes[i];
let name = attribute.name;
let value = attribute.value;
let hasSubstitution = false;
// name has substitution
if (name.indexOf(substitutionIndex) !== -1) {
hasSubstitution = true;
name = name.replace(substitutionRegex, replaceSubstitution);
if (name.indexOf(SUBSTITUION_INDEX) !== -1) {
name = name.replace(SUBSTITUTION_REGEX, replaceSubstitution);
// ensure substitution was with a non-empty string
if (name && typeof name === 'string') {
hasSubstitution = true;
}
// remove old attribute
node.removeAttribute(attribute.name);
attributesToRemove.push(attribute.name);
}
// value has substitution
if (value.indexOf(substitutionIndex) !== -1) {
// value has substitution - only check if name exists (only happens
// when name is a substitution with an empty value)
if (name && value.indexOf(SUBSTITUION_INDEX) !== -1) {
hasSubstitution = true;
value = value.replace(substitutionRegex, replaceSubstitution);
// contextual auto escape
if (name === 'href') {
// URI encode then only allow the : when used after http or https
// (will not allow any 'javascript:' or filter evasion techniques)
value = encodeURI(value).replace(':', function(match, index, string) {
var protocol = string.substring(index-5, index);
if (protocol.indexOf('http') !== -1) {
return match;
// if an uri attribute has been rejected
let isRejected = false;
value = value.replace(SUBSTITUTION_REGEX, function(match, index, offset) {
if (isRejected) {
return '';
}
let substitutionValue = values[parseInt(index, 10)];
// contextual auto-escaping:
// if attribute is a DOM Level 0 event then we need to ensure it
// is quoted
if (DOM_EVENTS.indexOf(name) !== -1 &&
typeof substitutionValue === 'string' &&
!WRAPPED_WITH_QUOTES_REGEX.test(substitutionValue) ) {
substitutionValue = '"' + substitutionValue + '"';
}
// contextual auto-escaping:
// if the attribute is a uri attribute then we need to uri encode it and
// remove bad protocols
else if (URI_ATTRIBUTES.indexOf(name) !== -1 ||
CUSTOM_URI_ATTRIBUTES_REGEX.test(name)) {
// percent encode if the value is inside of a query parameter
let queryParamIndex = value.indexOf('=');
if (queryParamIndex !== -1 && offset > queryParamIndex) {
substitutionValue = encodeURIComponent(substitutionValue);
}
// entity encode if value is part of the URL
else {
substitutionValue = encodeURI( encodeURIEntities(substitutionValue) );
// only allow the : when used after http or https otherwise reject
// the entire url (will not allow any 'javascript:' or filter
// evasion techniques)
if (offset === 0 && substitutionValue.indexOf(':') !== -1) {
let protocol = substitutionValue.substring(index-5, index);
if (protocol.indexOf('http') === -1) {
isRejected = true;
}
}
}
}
return '\\x' + match.charCodeAt(0).toString(16).toUpperCase();
});
// contextual auto-escaping:
// HTML encode attribute value if it is not a URL or URI to prevent
// DOM Level 0 event handlers from executing xss code
else if (typeof substitutionValue === 'string') {
substitutionValue = encodeAttributeHTMLEntities(substitutionValue);
}
return substitutionValue;
});
if (isRejected) {
value = '#' + REJECTION_STRING;
}

@@ -130,3 +340,3 @@ }

// setAttribute() does not need to be escaped to prevent XSS since it does
// all of that for use
// all of that for us
// @see https://www.mediawiki.org/wiki/DOM-based_XSS

@@ -139,4 +349,11 @@ if (tag || hasSubstitution) {

// remove placeholder attributes outside of the attribute loop since it
// will modify the attributes NamedNodeMap indices.
// @see https://github.com/straker/html-tagged-template/issues/13
attributesToRemove.forEach(function(attribute) {
node.removeAttribute(attribute);
});
// append the current node to a replaced parent
var parentNode;
let parentNode;
if (node.parentNode && node.parentNode._replacedWith) {

@@ -153,8 +370,15 @@ parentNode = node.parentNode;

// node value
if (node.nodeType === 3 && node.nodeValue.indexOf(substitutionIndex) !== -1) {
var nodeValue = node.nodeValue.replace(substitutionRegex, replaceSubstitution);
// --------------------------------------------------
// text content substitution
// --------------------------------------------------
if (node.nodeType === 3 && node.nodeValue.indexOf(SUBSTITUION_INDEX) !== -1) {
let nodeValue = node.nodeValue.replace(SUBSTITUTION_REGEX, replaceSubstitution);
// createTextNode() should not need to be escaped to prevent XSS?
var text = document.createTextNode(nodeValue);
let text = document.createTextNode(nodeValue);

@@ -167,7 +391,5 @@ // since the parent node has already gone through the iterator, we can use

// return an array of childNodes instead of an HTMLCollection, compliant with
// the new DOM spec to make collections an Array
// @see https://dom.spec.whatwg.org/#element-collections
// return the documentFragment for multiple nodes
if (template.content.childNodes.length > 1) {
return Array.from(template.content.childNodes);
return template.content;
}

@@ -174,0 +396,0 @@

module.exports = function (config) {
try {
require('dotenv').config();
} catch(e) {
}
if (!process.env.SAUCE_USERNAME || !process.env.SAUCE_ACCESS_KEY) {

@@ -25,7 +31,7 @@ console.log('Make sure the SAUCE_USERNAME and SAUCE_ACCESS_KEY environment variables are set.')

},
Windows_Firefox: {
Windows_Edge: {
base: 'SauceLabs',
platform: 'Windows 10',
browserName: 'firefox'
},
browserName: 'MicrosoftEdge'
}
}

@@ -40,3 +46,13 @@

],
reporters: ['progress', 'saucelabs'],
reporters: ['progress', 'saucelabs', 'coverage'],
preprocessors: {
'index.js': ['coverage']
},
coverageReporter: {
dir : 'coverage/',
reporters: [
{type: 'lcov', subdir: '.'},
{type: 'text-summary'}
]
},
port: 9876,

@@ -43,0 +59,0 @@ colors: true,

@@ -10,3 +10,3 @@ // Karma configuration

],
browsers: ['Chrome'],
browsers: ['Chrome', 'Firefox', 'Safari'],
reporters: ['progress', 'coverage'],

@@ -13,0 +13,0 @@ preprocessors: {

{
"name": "html-tagged-template",
"description": "Proposal to improve the DOM creation API so developers have a cleaner, simpler interface to DOM creation and manipulation.",
"version": "1.0.0",
"version": "2.0.0",
"main": "index.js",

@@ -34,2 +34,3 @@ "scripts": {

"coveralls": "^2.11.6",
"dotenv": "^2.0.0",
"gulp": "^3.9.1",

@@ -36,0 +37,0 @@ "gulp-connect": "^2.3.1",

// @see https://developers.google.com/closure/templates/docs/security
var xss = "javascript:/*</style></script>/**/ /<script>1/(alert(1337))//</script>";
html`<a href="${xss}"
document.body.appendChild(html`<a href="${xss}"
onclick="${xss}"

@@ -13,4 +13,2 @@ >${xss}</a>

}
</style>`.forEach(function(node) {
document.body.appendChild(node);
});
</style>`);

@@ -21,11 +21,17 @@ [![Build Status](https://travis-ci.org/straker/html-tagged-template.svg?branch=master)](https://travis-ci.org/straker/html-tagged-template)

// returns an <input> tag with all attributes set
let el = html`<input type="number" min="${min}" max="${max}" name="number" id="number" class="number-input" ${ (disabled ? 'disabled' : '') }/>`;
document.body.appendChild(el);
let els = html`<tr></tr><tr></tr>`
els.forEach(function(el) {
document.body.appendChild(el);
});
// returns a DocumentFragment with two <tr> elements as children
let el = html`<tr></tr><tr></tr>`
document.body.appendChild(el);
```
## Contributing
The only way this proposal will continue forward is with help from the community. If you would like to see the `html` function in the web, please upvote the [proposal on the W3C DOM repo](https://github.com/whatwg/dom/issues/150).
If you find a bug or an XSS case that should to be handled, please submit an issue, or even better a PR with the relevant code to reproduce the error in the [xss test](test/xss.test.js).
## Problem Space

@@ -32,0 +38,0 @@

@@ -110,7 +110,7 @@ describe('HTML parser', function() {

// correct node
expect(nodes).to.be.instanceof(Array);
expect(nodes.length).to.equal(2);
expect(nodes).to.be.instanceof(DocumentFragment);
expect(nodes.childNodes.length).to.equal(2);
// correct first child
var tr = nodes[0];
var tr = nodes.querySelectorAll('tr')[0];
expect(tr.nodeName).to.equal('TR');

@@ -123,3 +123,3 @@ expect(tr.attributes.length, 'more than 1 attribute').to.equal(0);

// correct second child
var tr2 = nodes[0];
var tr2 = nodes.querySelectorAll('tr')[1];
expect(tr2.nodeName).to.equal('TR');

@@ -126,0 +126,0 @@ expect(tr2.attributes.length, 'more than 1 attribute').to.equal(0);

@@ -29,3 +29,3 @@ describe('Substitution expressions', function() {

it('should add attribute names or values from variables', function() {
var el = html`<input type="number" min="${min}" name="number" id="number" class="number-input" ${ (disabled ? 'disabled' : '') }/>`;
var el = html`<input type="number" min="${min}" name="number" id="number" class="number-input" max="${max}" ${ (disabled ? 'disabled' : '') }/>`;

@@ -36,3 +36,3 @@ // correct node

// correct attributes
expect(el.attributes.length).to.equal(6);
expect(el.attributes.length).to.equal(7);
expect(el.type).to.equal('number');

@@ -51,2 +51,40 @@ expect(el.min).to.equal('0');

it('should skip empty attributes', function() {
var emptyDisabled = false;
var el = html`<input type="number" min="${min}" name="number" id="number" class="number-input" max="${max}" ${ (emptyDisabled ? 'disabled' : '') }/>`;
// correct node
expect(el.nodeName).to.equal('INPUT');
// correct attributes
expect(el.attributes.length).to.equal(6);
expect(el.type).to.equal('number');
expect(el.min).to.equal('0');
expect(el.name).to.equal('number');
expect(el.id).to.equal('number');
expect(el.className).to.equal('number-input');
expect(el.disabled).to.equal(false);
// no extraneous side-effects
expect(el.children.length).to.equal(0);
expect(el.parentElement).to.be.null;
expect(el.textContent).to.be.empty;
});
it('should skip non-valid attribute substituted names', function() {
var nonValidAttrName = [];
var el = html`<div ${nonValidAttrName}="hello"/>`;
// correct node
expect(el.nodeName).to.equal('DIV');
// correct attributes
expect(el.attributes.length).to.equal(0);
// no extraneous side-effects
expect(el.children.length).to.equal(0);
expect(el.parentElement).to.be.null;
expect(el.textContent).to.be.empty;
});
it('should move any children from a substituted node to the new node', function() {

@@ -53,0 +91,0 @@ var el = html`<h${heading}><span>Hello</span></h${heading}>`;

var counter = 0;
describe('XSS Attack Vectors', function() {

@@ -45,11 +46,9 @@ // Modified XSS String

el.click();
expect(x).to.equal(xss);
});
// test fails but we're not sure how best to handle this
// it('should prevent injection into quoted event handler', function() {
// var el = html`<a href='#' onclick="${xss}">XSS &lt;p&gt; tag</a>`;
// document.body.appendChild(el);
// el.click();
// });
it('should prevent injection into quoted event handler', function() {
var el = html`<a href='#' onclick="${xss}">XSS &lt;p&gt; tag</a>`;
document.body.appendChild(el);
el.click();
});

@@ -78,6 +77,80 @@ it('should prevent injection into CSS unquote property', function() {

it('should prevent injection into HREF attribute of <a> tag', function() {
var el = html`<a target='_blank' href="${xss}">XSS'ed Link</a>`;
var el = html`<a href="${xss}">XSS'ed Link</a>`;
document.body.appendChild(el);
el.click();
});
});
it('should prevent against clobbering of /attributes/', function() {
var el = html`<form id="f" action="${xss}" onsubmit="return false;">
<input type="radio" name="attributes"//>
<input type="submit" />
</form>`;
document.body.appendChild(el);
// el.submit() does not trigger a submit event, so we need to click the submit button
// @see http://stackoverflow.com/questions/11557994/jquery-submit-vs-javascript-submit
el.querySelector('input[type="submit"]').click();
});
it('should prevent injection out of a tag name by throwing an error', function() {
var func = function() {
var el = html`<h${xss}></h${xss}>`;
document.body.appendChild(el);
};
expect(func).to.throw;
});
it('should prevent xss protocol URLs by rejecting them', function() {
var el = html`<a href="${xss}"></a>`;
document.body.appendChild(el);
el.click();
expect(el.getAttribute('href')[0]).to.equal('#');
});
it('should not prevent javascript protocol if it was a safe string', function() {
var value = 'foo/bar&baz/boo';
var el = html`<a href="javascript:void(0);">`;
expect(el.getAttribute('href')).to.equal('javascript:void(0);');
});
it('should prevent injection into uri custom attributes', function() {
var el = html`<a href="#" data-uri="${xss}">`
document.body.appendChild(el);
el.href = el.getAttribute('data-uri');
el.click();
});
it('should entity escape URLs', function() {
var value = 'foo/bar&baz/boo';
var el = html`<a href="${value}">`;
expect(el.getAttribute('href')).to.equal('foo/bar&amp;baz/boo');
});
it('should percent encode inside URL query', function() {
var value = 'bar&baz=boo';
var el = html`<a href="foo?q=${value}">`;
expect(el.getAttribute('href')).to.equal('foo?q=bar%26baz%3Dboo');
});
it('should percent encode inside URL query and entity escape if not', function() {
var value = 'bar&baz=boo';
var el = html`<a href="foo/${value}/bar?q=${value}">`;
expect(el.getAttribute('href')).to.equal('foo/bar&amp;baz=boo/bar?q=bar%26baz%3Dboo');
});
it('should reject a URL outright if it has the wrong protocol', function() {
var protocol = 'javascript:alert(1337)';
var value = '/foo&bar/bar';
var el = html`<a href="${protocol}/bar${value}">`;
expect(el.getAttribute('href')[0]).to.equal('#');
expect(el.getAttribute('href').indexOf('/bar')).to.equal(-1);
});
});

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

SocketSocket SOC 2 Logo

Product

  • Package Alerts
  • Integrations
  • Docs
  • Pricing
  • FAQ
  • Roadmap
  • Changelog

Packages

npm

Stay in touch

Get open source security insights delivered straight into your inbox.


  • Terms
  • Privacy
  • Security

Made with ⚡️ by Socket Inc