├── .gitignore ├── .jshintrc ├── .travis.yml ├── README.md ├── bower.json ├── gulpfile.js ├── index.html ├── index.js ├── karma.conf-ci.js ├── karma.conf.js ├── package.json ├── page ├── index.html └── xss.js ├── references.md ├── securitySpec.md └── test ├── htmlPaser.test.js ├── substitution.test.js └── xss.test.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .DS_Store 3 | coverage 4 | sauce_connect.log 5 | .env -------------------------------------------------------------------------------- /.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "bitwise" : false, // Prohibit bitwise operators (&, |, ^, etc.). 3 | "curly" : true, // Require {} for every new block or scope. 4 | "eqeqeq" : true, // Require triple equals i.e. `===`. 5 | "forin" : true, // Tolerate `for in` loops without `hasOwnPrototype`. 6 | "immed" : true, // Require immediate invocations to be wrapped in parens e.g. `( function(){}() );` 7 | "latedef" : false, // Prohibit variable use before definition. 8 | "newcap" : true, // Require capitalization of all constructor functions e.g. `new F()`. 9 | "noarg" : true, // Prohibit use of `arguments.caller` and `arguments.callee`. 10 | "noempty" : true, // Prohibit use of empty blocks. 11 | "nonew" : true, // Prohibit use of constructors for side-effects. 12 | "plusplus" : false, // Prohibit use of `++` & `--`. 13 | "regexp" : true, // Prohibit `.` and `[^...]` in regular expressions. 14 | "undef" : false, // Require all non-global variables be declared before they are used. 15 | "strict" : true, // Require `use strict` pragma in every file. 16 | "trailing" : true, // Prohibit trailing whitespaces. 17 | "browser" : true, // Standard browser globals e.g. `window`, `document`. 18 | "boss" : true , // Suppress warnings about assignments in comparisons 19 | "esversion" : 6 // Use ECMAScript 6 specific syntax 20 | } -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "4.0" 4 | addons: 5 | firefox: "42.0" 6 | before_script: 7 | - "export DISPLAY=:99.0" 8 | - "sh -e /etc/init.d/xvfb start" 9 | - sleep 3 # give xvfb some time to start 10 | after_script: "cat ./coverage/lcov.info | ./node_modules/coveralls/bin/coveralls.js" -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Build Status](https://travis-ci.org/straker/html-tagged-template.svg?branch=master)](https://travis-ci.org/straker/html-tagged-template) 2 | [![Coverage Status](https://coveralls.io/repos/github/straker/html-tagged-template/badge.svg?branch=master)](https://coveralls.io/github/straker/html-tagged-template?branch=master) 3 | 4 | # Proposal 5 | 6 | Improve the DOM creation API so developers have a cleaner, simpler interface to DOM creation and manipulation. 7 | 8 | ## Installing 9 | 10 | `npm install html-tagged-template` 11 | 12 | or with Bower 13 | 14 | `bower install html-tagged-template` 15 | 16 | ## Usage 17 | 18 | ```js 19 | let min = 0, max = 99, disabled = true; 20 | 21 | // returns an tag with all attributes set 22 | // the use of ?= denotes an optional attribute which will only be added if the 23 | // value is true 24 | let el = html``; 25 | document.body.appendChild(el); 26 | 27 | // returns a DocumentFragment with two elements as children 28 | let el = html`` 29 | document.body.appendChild(el); 30 | ``` 31 | 32 | ### Optional Attributes 33 | 34 | To add an attribute only when it's value is true (such as `disabled`), use `attrName?="${value}"`. If the value is true, the attribute will be added in the output, otherwise it will be omitted from the output. 35 | 36 | ## Contributing 37 | 38 | 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). 39 | 40 | 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). 41 | 42 | ## Problem Space 43 | 44 | The DOM creation API is a bit cumbersome to work with. To create a single element with several attributes requires several lines of code that repeat the same thing. The DOM selection API has received needed features that allow developers to do most DOM manipulation without needing a library. However, the DOM creation API still leaves something to be desired which sways developers from using it. 45 | 46 | Below are just a few examples of how DOM creation requires multiple lines of code to accomplish simple tasks and how developers tend to work around the API to gain access to a much simpler interface. 47 | 48 | ```js 49 | /* 50 | Create a single element with attributes: 51 | 52 | */ 53 | let input = document.createElement('input'); 54 | input.type = "number"; 55 | input.min = 0; 56 | input.max = 99; 57 | input.name = 'number'; 58 | input.id = 'number'; 59 | input.classList.add('number-input'); 60 | input.disabled = true; 61 | document.body.appendChild(input); 62 | 63 | // or the hacky way - create a throwaway parent node just to use innerHTML 64 | let div = document.createElement('div'); 65 | div.innerHTML = ''; 66 | document.body.appendChild(div.firstChild); 67 | 68 | 69 | /* 70 | Create an element with child elements: 71 |
72 |
73 |
74 |
Hello
75 |
76 |
77 |
78 | */ 79 | // use document fragment to batch appendChild calls for good performance 80 | let frag = document.createDocumentFragment(); 81 | let div = document.createElement('div'); 82 | div.classList.add('container'); 83 | frag.appendChild(div); 84 | 85 | let row = document.createElement('div'); 86 | row.classList.add('row'); 87 | div.appendChild(row); 88 | 89 | let col = document.createElement('div'); 90 | col.classList.add('col'); 91 | row.appendChild(col); 92 | 93 | let child = document.createElement('div'); 94 | child.appendChild(document.createTextNode('Hello')); // or child.textContext = 'Hello'; 95 | col.appendChild(child); 96 | document.body.appendChild(frag); 97 | 98 | // or the convenient way using innerHTML 99 | let div = document.createElement('div'); 100 | div.classList.add('container'); 101 | div.innerHTML = '
Hello
'; 102 | document.body.appendChild(div); 103 | 104 | 105 | /* 106 | Create sibling elements to be added to a parent element: 107 | 108 | 111 | 112 | 113 | 119 | */ 120 | let frag = document.createDocumentFragment(); 121 | let li = document.createElement('li'); 122 | li.textContent = 'Plane'; 123 | frag.appendChild(li); 124 | 125 | li = document.createElement('li'); 126 | li.textContent = 'Boat'; 127 | frag.appendChild(li); 128 | 129 | li = document.createElement('li'); 130 | li.textContent = 'Bike'; 131 | frag.appendChild(li); 132 | document.querySelector('#list').appendChild(frag); 133 | 134 | // or if you have the ability to create it through a loop 135 | let frag = document.createDocumentFragment(); 136 | ['Plane', 'Boat', 'Bike'].forEach(function(item) { 137 | let li = document.createElement('li'); 138 | li.textContent = item; 139 | frag.appendChild(li); 140 | }); 141 | document.querySelector('#list').appendChild(frag); 142 | ``` 143 | 144 | ## Proposed Solution 145 | 146 | We propose that a global tagged template string function called `html` provide the interface to accept template strings as input and return the parsed DOM elements. 147 | 148 | ```js 149 | let min = 0, max = 99, disabled = true, text = 'Hello'; 150 | 151 | // single element with attributes 152 | html``; 153 | 154 | // single element with child elements 155 | html`
156 |
157 |
158 |
${text}
159 |
160 |
161 |
`; 162 | 163 | // sibling elements 164 | html`
  • Plane
  • 165 |
  • Boat
  • 166 |
  • Bike
  • `; 167 | ``` 168 | 169 | ## Goals 170 | 171 | 1. [Easy to Use](#easy-to-use) 172 | 1. [Secure](#secure) 173 | 174 | ### Easy to Use 175 | 176 | This proposal wouldn't exist if creating the DOM was easy. Any improvement to the DOM creation API would essentially need to replace `innerHTML` with something better and just as easy (if not easier), otherwise developers will continue to use it to work around the API. 177 | 178 | #### Proposed Solution 179 | 180 | To solve this problem, we propose a new API that will allow developers to create single, sibling, or nested child nodes with a single function call. With the addition of template strings to ECMAScript 2015, [we and others](https://lists.w3.org/Archives/Public/www-dom/2011OctDec/0170.html) feel that they are the cleanest, simplest, and most intuitive interface for DOM creation. 181 | 182 | ECMAScript 2015 also introduced [tagged template strings](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/template_strings#Tagged_template_strings) which would allow a function to accept a template string as input and return DOM. Tagged template strings also have the advantage that they can understand where variables were used in the string and be able to apply security measures to prevent XSS. 183 | 184 | #### Other Solutions 185 | 186 | ##### Object-like notation 187 | 188 | Object-like notation applies object literals to DOM creation. Instead of using a string of HTML, object-like notation uses property names and values to construct a DOM object. 189 | 190 | ```js 191 | createElement('input', {type: 'number', min: 0, max: 99, name: 'number', id: 'number', className: 'number-input', disabled: true, inside: document.body}); 192 | ``` 193 | 194 | However, this solution suffers from a few problems. First, it tends to combine content attributes (HTML attributes such as `id` or `name`) with IDL attributes (JavaScript properties such as `textContent` or `className`) which can lead to developer confusion as they don't know which attribute to use or how. For example, `class` is a reserved word in JavaScript and couldn't be used as a property name, even though it is a content attribute, unless it was always quoted. It also tends to add helper attributes (such as `contents`) which add to the confusion. 195 | 196 | Second, it adds verbosity and complexity to the creation of nested nodes that provide only a slightly better interface to DOM creation than the standard `createElement` and `appendChild` methods. Since developers already use `innerHTML` to avoid these methods, it would seem unlikely that they would give up the convenience of `innerHTML` for a something more complex and verbose. 197 | 198 | ```js 199 | createElement('div', {className: 'container', inside: document.body, contents: [ 200 | {tag: 'div', className: 'row', contents: [ 201 | {tag: 'div', className: 'col', contents: [ 202 | {tag: 'div', contents: ['Hello']} 203 | ]} 204 | ]} 205 | ]}); 206 | ``` 207 | 208 | ### Secure 209 | 210 | XSS attacks via string concatenation are among the most prevalent types of security threats the web development world faces. Tagged template strings provide a unique opportunity to make creating DOM much more secure than string concatenation ever could. Tagged template strings know exactly where the user substitution expressions are located in the string, enabling us to apply preventative measures to help ensure the resulting DOM is safe from XSS attacks. 211 | 212 | #### Proposed Solution 213 | 214 | There have been two proposed solutions for making template strings secure against XSS: [E4H](http://www.hixie.ch/specs/e4h/strawman), championed by Ian Hixie, and [contextual auto escaping](https://js-quasis-libraries-and-repl.googlecode.com/svn/trunk/safetemplate.html#security_under_maintenance), championed by Mike Samuel. 215 | 216 | E4H uses an AST to construct the DOM, ensuring that substitutions are made safe against element and attribute injection. Contextual auto escaping tries to understand the context of the attribute or element in the DOM and correctly escape the substitution based on it's context. 217 | 218 | We propose combining the best ideas from both E4H and contextual auto escaping and avoiding the problems that both encountered. First, the template string is sanitized by removing all substitution expressions (and all XSS attack vectors with them), while leaving placeholders in the resulting string that identify the substitution that belonged there. Next, the string is passed to an HTML `template` tag using `innerHTML`, which runs the string through the HTML parser and properly creates elements out of context (such as `` elements). Finally, all placeholders are identified and replaced with their substitution expression using the DOM APIs `createElement`, `createTextNode`, and `setAttribute`, and then using contextual auto escaping to prevent further XSS attack vectors. -------------------------------------------------------------------------------- /bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "html-tagged-template", 3 | "description": "Proposal to improve the DOM creation API so developers have a cleaner, simpler interface to DOM creation and manipulation.", 4 | "keywords": [ 5 | "proposal", 6 | "HTML", 7 | "DOM", 8 | "tagged", 9 | "template", 10 | "string" 11 | ], 12 | "authors": [ 13 | "Steven Lambert (http://sklambert.com/)", 14 | "Josh Crowther " 15 | ], 16 | "homepage": "https://github.com/straker/html-tagged-template", 17 | "repository": { 18 | "type": "git", 19 | "url": "https://github.com/straker/html-tagged-template.git" 20 | }, 21 | "main": [ 22 | "index.js", 23 | "index.html" 24 | ], 25 | "moduleType": "globals", 26 | "license": "MIT", 27 | "ignore": [ 28 | "**/.*", 29 | "node_modules", 30 | "bower_components", 31 | "test", 32 | "tests" 33 | ] 34 | } 35 | -------------------------------------------------------------------------------- /gulpfile.js: -------------------------------------------------------------------------------- 1 | var gulp = require('gulp'); 2 | var jshint = require('gulp-jshint'); 3 | var Server = require('karma').Server; 4 | var connect = require('gulp-connect'); 5 | 6 | gulp.task('lint', function() { 7 | return gulp.src('index.js') 8 | .pipe(jshint()) 9 | .pipe(jshint.reporter('jshint-stylish')) 10 | }); 11 | 12 | gulp.task('test', function(done) { 13 | new Server({ 14 | configFile: __dirname + '/karma.conf.js', 15 | }, done).start(); 16 | }); 17 | 18 | gulp.task('test-ci', function(done) { 19 | new Server({ 20 | configFile: __dirname + '/karma.conf-ci.js', 21 | }, done).start(); 22 | }); 23 | 24 | gulp.task('connect', function() { 25 | connect.server({ 26 | livereload: true 27 | }); 28 | }); 29 | 30 | gulp.task('watch', function() { 31 | gulp.watch('index.js', ['lint']); 32 | }); 33 | 34 | gulp.task('default', ['lint', 'test', 'watch']); -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | (function(window) { 2 | "use strict"; 3 | 4 | // test for es6 support of needed functionality 5 | try { 6 | // template tag and Array.from support 7 | if (!('content' in document.createElement('template') && 'from' in Array)) { 8 | throw new Error(); 9 | } 10 | } 11 | catch (e) { 12 | // missing support; 13 | console.log('Your browser does not support the needed functionality to use the html tagged template'); 14 | return; 15 | } 16 | 17 | if (typeof window.html === 'undefined') { 18 | 19 | // -------------------------------------------------- 20 | // constants 21 | // -------------------------------------------------- 22 | 23 | const SUBSTITUTION_INDEX = 'substitutionindex:'; // tag names are always all lowercase 24 | const SUBSTITUTION_REGEX = new RegExp(SUBSTITUTION_INDEX + '([0-9]+):', 'g'); 25 | 26 | // rejection string is used to replace xss attacks that cannot be escaped either 27 | // because the escaped string is still executable 28 | // (e.g. setTimeout(/* escaped string */)) or because it produces invalid results 29 | // (e.g. where xss='><') 176 | // instead of replacing the tag name we'll just let the error be thrown 177 | tag = document.createElement(nodeName); 178 | 179 | // mark that this node needs to be cleaned up later with the newly 180 | // created node 181 | node._replacedWith = tag; 182 | 183 | // use insertBefore() instead of replaceChild() so that the node Iterator 184 | // doesn't think the new tag should be the next node 185 | node.parentNode.insertBefore(tag, node); 186 | } 187 | 188 | // special case for script tags: 189 | // using innerHTML with a string that contains a script tag causes the script 190 | // tag to not be executed when added to the DOM. We'll need to create a script 191 | // tag and append its contents which will make it execute correctly. 192 | // @see http://stackoverflow.com/questions/1197575/can-scripts-be-inserted-with-innerhtml 193 | else if (node.nodeName === 'SCRIPT') { 194 | let script = document.createElement('script'); 195 | tag = script; 196 | 197 | node._replacedWith = script; 198 | node.parentNode.insertBefore(script, node); 199 | } 200 | 201 | 202 | 203 | 204 | 205 | // -------------------------------------------------- 206 | // attribute substitution 207 | // -------------------------------------------------- 208 | 209 | let attributes; 210 | if (node.attributes) { 211 | 212 | // if the attributes property is not of type NamedNodeMap then the DOM 213 | // has been clobbered. E.g.
    . 214 | // We'll manually build up an array of objects that mimic the Attr 215 | // object so the loop will still work as expected. 216 | if ( !(node.attributes instanceof NamedNodeMap) ) { 217 | 218 | // first clone the node so we can isolate it from any children 219 | let temp = node.cloneNode(); 220 | 221 | // parse the node string for all attributes 222 | let attributeMatches = temp.outerHTML.match(ATTRIBUTE_PARSER_REGEX); 223 | 224 | // get all attribute names and their value 225 | attributes = []; 226 | for (let i = 0; i < attributeMatches.length; i++) { 227 | let attributeName = attributeMatches[i].trim().split('=')[0]; 228 | let attributeValue = node.getAttribute(attributeName); 229 | 230 | attributes.push({ 231 | name: attributeName, 232 | value: attributeValue 233 | }); 234 | } 235 | } 236 | else { 237 | // Windows 10 Firefox 44 will shift the attributes NamedNodeMap and 238 | // push the attribute to the end when using setAttribute(). We'll have 239 | // to clone the NamedNodeMap so the order isn't changed for setAttribute() 240 | attributes = Array.from(node.attributes); 241 | } 242 | 243 | for (let i = 0; i < attributes.length; i++) { 244 | let attribute = attributes[i]; 245 | let name = attribute.name; 246 | let value = attribute.value; 247 | let hasSubstitution = false; 248 | 249 | // name has substitution 250 | if (name.indexOf(SUBSTITUTION_INDEX) !== -1) { 251 | name = name.replace(SUBSTITUTION_REGEX, replaceSubstitution); 252 | 253 | // ensure substitution was with a non-empty string 254 | if (name && typeof name === 'string') { 255 | hasSubstitution = true; 256 | } 257 | 258 | // remove old attribute 259 | attributesToRemove.push(attribute.name); 260 | } 261 | 262 | // value has substitution - only check if name exists (only happens 263 | // when name is a substitution with an empty value) 264 | if (name && value.indexOf(SUBSTITUTION_INDEX) !== -1) { 265 | hasSubstitution = true; 266 | 267 | // if an uri attribute has been rejected 268 | let isRejected = false; 269 | 270 | value = value.replace(SUBSTITUTION_REGEX, function(match, index, offset) { 271 | if (isRejected) { 272 | return ''; 273 | } 274 | 275 | let substitutionValue = values[parseInt(index, 10)]; 276 | 277 | // contextual auto-escaping: 278 | // if attribute is a DOM Level 0 event then we need to ensure it 279 | // is quoted 280 | if (DOM_EVENTS.indexOf(name) !== -1 && 281 | typeof substitutionValue === 'string' && 282 | !WRAPPED_WITH_QUOTES_REGEX.test(substitutionValue) ) { 283 | substitutionValue = '"' + substitutionValue + '"'; 284 | } 285 | 286 | // contextual auto-escaping: 287 | // if the attribute is a uri attribute then we need to uri encode it and 288 | // remove bad protocols 289 | else if (URI_ATTRIBUTES.indexOf(name) !== -1 || 290 | CUSTOM_URI_ATTRIBUTES_REGEX.test(name)) { 291 | 292 | // percent encode if the value is inside of a query parameter 293 | let queryParamIndex = value.indexOf('='); 294 | if (queryParamIndex !== -1 && offset > queryParamIndex) { 295 | substitutionValue = encodeURIComponent(substitutionValue); 296 | } 297 | 298 | // entity encode if value is part of the URL 299 | else { 300 | substitutionValue = encodeURI( encodeURIEntities(substitutionValue) ); 301 | 302 | // only allow the : when used after http or https otherwise reject 303 | // the entire url (will not allow any 'javascript:' or filter 304 | // evasion techniques) 305 | if (offset === 0 && substitutionValue.indexOf(':') !== -1) { 306 | let protocol = substitutionValue.substring(0, 5); 307 | if (protocol.indexOf('http') === -1) { 308 | isRejected = true; 309 | } 310 | } 311 | } 312 | } 313 | 314 | // contextual auto-escaping: 315 | // HTML encode attribute value if it is not a URL or URI to prevent 316 | // DOM Level 0 event handlers from executing xss code 317 | else if (typeof substitutionValue === 'string') { 318 | substitutionValue = encodeAttributeHTMLEntities(substitutionValue); 319 | } 320 | 321 | return substitutionValue; 322 | }); 323 | 324 | if (isRejected) { 325 | value = '#' + REJECTION_STRING; 326 | } 327 | } 328 | 329 | // add the attribute to the new tag or replace it on the current node 330 | // setAttribute() does not need to be escaped to prevent XSS since it does 331 | // all of that for us 332 | // @see https://www.mediawiki.org/wiki/DOM-based_XSS 333 | if (tag || hasSubstitution) { 334 | let el = (tag || node); 335 | 336 | // optional attribute 337 | if (name.substr(-1) === '?') { 338 | el.removeAttribute(name); 339 | 340 | if (value === 'true') { 341 | name = name.slice(0, -1); 342 | el.setAttribute(name, ''); 343 | } 344 | } 345 | else { 346 | el.setAttribute(name, value); 347 | } 348 | } 349 | } 350 | } 351 | 352 | // remove placeholder attributes outside of the attribute loop since it 353 | // will modify the attributes NamedNodeMap indices. 354 | // @see https://github.com/straker/html-tagged-template/issues/13 355 | attributesToRemove.forEach(function(attribute) { 356 | node.removeAttribute(attribute); 357 | }); 358 | 359 | // append the current node to a replaced parent 360 | let parentNode; 361 | if (node.parentNode && node.parentNode._replacedWith) { 362 | parentNode = node.parentNode; 363 | node.parentNode._replacedWith.appendChild(node); 364 | } 365 | 366 | // remove the old node from the DOM 367 | if ((node._replacedWith && node.childNodes.length === 0) || 368 | (parentNode && parentNode.childNodes.length === 0) ){ 369 | (parentNode || node).remove(); 370 | } 371 | 372 | 373 | 374 | 375 | 376 | // -------------------------------------------------- 377 | // text content substitution 378 | // -------------------------------------------------- 379 | 380 | if (node.nodeType === 3 && node.nodeValue.indexOf(SUBSTITUTION_INDEX) !== -1) { 381 | let nodeValue = node.nodeValue.replace(SUBSTITUTION_REGEX, replaceSubstitution); 382 | 383 | // createTextNode() should not need to be escaped to prevent XSS? 384 | let text = document.createTextNode(nodeValue); 385 | 386 | // since the parent node has already gone through the iterator, we can use 387 | // replaceChild() here 388 | node.parentNode.replaceChild(text, node); 389 | } 390 | } 391 | 392 | // return the documentFragment for multiple nodes 393 | if (template.content.childNodes.length > 1) { 394 | return template.content; 395 | } 396 | 397 | return template.content.firstChild; 398 | }; 399 | } 400 | 401 | })(window); 402 | -------------------------------------------------------------------------------- /karma.conf-ci.js: -------------------------------------------------------------------------------- 1 | module.exports = function (config) { 2 | try { 3 | require('dotenv').config(); 4 | } catch(e) { 5 | 6 | } 7 | 8 | if (!process.env.SAUCE_USERNAME || !process.env.SAUCE_ACCESS_KEY) { 9 | console.log('Make sure the SAUCE_USERNAME and SAUCE_ACCESS_KEY environment variables are set.') 10 | process.exit(1) 11 | } 12 | 13 | // Browsers to run on Sauce Labs 14 | // Check out https://saucelabs.com/platforms for all browser/OS combos 15 | var customLaunchers = { 16 | OSX_Chrome: { 17 | base: 'SauceLabs', 18 | platform: 'OS X 10.11', 19 | browserName: 'chrome', 20 | }, 21 | OSX_Firefox: { 22 | base: 'SauceLabs', 23 | platform: 'OS X 10.11', 24 | browserName: 'firefox' 25 | }, 26 | Windows_Chrome: { 27 | base: 'SauceLabs', 28 | platform: 'Windows 10', 29 | browserName: 'chrome', 30 | }, 31 | Windows_Edge: { 32 | base: 'SauceLabs', 33 | platform: 'Windows 10', 34 | browserName: 'MicrosoftEdge' 35 | } 36 | } 37 | 38 | config.set({ 39 | basePath: '', 40 | frameworks: ['mocha', 'chai'], 41 | files: [ 42 | 'index.js', 43 | 'test/*.js' 44 | ], 45 | reporters: ['progress', 'saucelabs', 'coverage'], 46 | preprocessors: { 47 | 'index.js': ['coverage'] 48 | }, 49 | coverageReporter: { 50 | dir : 'coverage/', 51 | reporters: [ 52 | {type: 'lcov', subdir: '.'}, 53 | {type: 'text-summary'} 54 | ] 55 | }, 56 | port: 9876, 57 | colors: true, 58 | logLevel: config.LOG_DEBUG, 59 | sauceLabs: { 60 | testName: 'http-tagged-template Test Suite', 61 | recordScreenshots: false, 62 | connectOptions: { 63 | port: 5757, 64 | logfile: 'sauce_connect.log' 65 | }, 66 | public: 'public' 67 | }, 68 | // Increase timeout in case connection in CI is slow 69 | captureTimeout: 120000, 70 | customLaunchers: customLaunchers, 71 | browsers: Object.keys(customLaunchers), 72 | singleRun: true 73 | }) 74 | } -------------------------------------------------------------------------------- /karma.conf.js: -------------------------------------------------------------------------------- 1 | // Karma configuration 2 | module.exports = function(config) { 3 | config.set({ 4 | basePath: '', 5 | frameworks: ['mocha', 'chai'], 6 | files: [ 7 | 'index.js', 8 | 'test/*.js' 9 | ], 10 | browsers: ['Chrome', 'Firefox', 'Safari'], 11 | reporters: ['progress', 'coverage'], 12 | preprocessors: { 13 | 'index.js': ['coverage'] 14 | }, 15 | coverageReporter: { 16 | dir : 'coverage/', 17 | reporters: [ 18 | {type: 'lcov', subdir: '.'}, 19 | {type: 'text-summary'} 20 | ] 21 | } 22 | }); 23 | }; 24 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "html-tagged-template", 3 | "description": "Proposal to improve the DOM creation API so developers have a cleaner, simpler interface to DOM creation and manipulation.", 4 | "version": "2.2.0", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "gulp test-ci" 8 | }, 9 | "repository": { 10 | "type": "git", 11 | "url": "https://github.com/straker/html-tagged-template.git" 12 | }, 13 | "keywords": [ 14 | "proposal", 15 | "HTML", 16 | "DOM", 17 | "tagged", 18 | "template", 19 | "string" 20 | ], 21 | "author": "Steven Lambert (http://sklambert.com/)", 22 | "contributors": [ 23 | "Josh Crowther " 24 | ], 25 | "license": "MIT", 26 | "bugs": { 27 | "url": "https://github.com/straker/html-tagged-template/issues" 28 | }, 29 | "homepage": "https://github.com/straker/html-tagged-template", 30 | "dependencies": {}, 31 | "devDependencies": { 32 | "chai": "^3.5.0", 33 | "coveralls": "^2.11.6", 34 | "dotenv": "^2.0.0", 35 | "gulp": "^3.9.1", 36 | "gulp-connect": "^2.3.1", 37 | "gulp-jshint": "^2.0.0", 38 | "gulp-mocha": "^2.2.0", 39 | "jshint": "^2.9.1", 40 | "jshint-stylish": "^2.1.0", 41 | "karma": "^0.13.21", 42 | "karma-chai": "^0.1.0", 43 | "karma-chrome-launcher": "^0.2.2", 44 | "karma-coverage": "^0.5.3", 45 | "karma-firefox-launcher": "^0.1.7", 46 | "karma-ie-launcher": "^0.2.0", 47 | "karma-mocha": "^0.2.1", 48 | "karma-safari-launcher": "^0.1.1", 49 | "karma-sauce-launcher": "^0.3.0", 50 | "mocha": "^2.4.5" 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /page/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 20 | 21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /page/xss.js: -------------------------------------------------------------------------------- 1 | // @see https://developers.google.com/closure/templates/docs/security 2 | var xss = "javascript:/*/**/ /"; 3 | document.body.appendChild(html`${xss} 6 | 7 | `); -------------------------------------------------------------------------------- /references.md: -------------------------------------------------------------------------------- 1 | * don't run template through HTML parser - https://lists.w3.org/Archives/Public/www-dom/2011OctDec/0170.html 2 | * should properly create nodes that don't have context (e.g. `foo`) - https://lists.w3.org/Archives/Public/public-script-coord/2013JanMar/0263.html 3 | * safe inject with `
    ` or `
    ` - https://lists.w3.org/Archives/Public/public-script-coord/2013JanMar/0268.html 4 | * should be able to create tag names such as `` - https://lists.w3.org/Archives/Public/public-script-coord/2013JanMar/0276.html 5 | * untrusted input gets added to DOM as text nodes (prevents XSS by making the XSS code benign) - https://lists.w3.org/Archives/Public/public-script-coord/2013JanMar/0207.html 6 | * handle XSS via CSS, URIs, and scripts - https://lists.w3.org/Archives/Public/public-script-coord/2013JanMar/0269.html 7 | ```js 8 | var data = "javascript:doEvil()"; 9 | `Hello, World!` 10 | 11 | var data = "expression(doEvil())"; 12 | `` 13 | 14 | `` 15 | ``` 16 | * E4H goals - https://lists.w3.org/Archives/Public/public-script-coord/2013JanMar/0278.html 17 | * examples of XSS riddled code where auto-escaping could fail - https://lists.w3.org/Archives/Public/public-script-coord/2013JanMar/0286.html 18 | * add quotes around unquoted attributes? - https://lists.w3.org/Archives/Public/public-script-coord/2013JanMar/0289.html 19 | * set an attribute value with having to account for whether or not the passed value included " or ' or whitespace (in case it's unquoted) -https://github.com/whatwg/dom/issues/150#issuecomment-182251393 20 | * HTML parser corner cases that produce unexpected results - https://lists.w3.org/Archives/Public/public-script-coord/2013JanMar/0290.html 21 | * `` what ever `${...}` expands to should stay withing the `example` attribute value and not create more attributes or DOM (XSS) - https://lists.w3.org/Archives/Public/public-script-coord/2013JanMar/0281.html 22 | * E4H is safe against element/attribute injection but not truly safe against all XSS, `...` `${x}` is still vulnerable to javascript injection - https://lists.w3.org/Archives/Public/public-script-coord/2013JanMar/0283.html 23 | * auto-escape corner cases and problems - https://lists.w3.org/Archives/Public/public-script-coord/2013JanMar/0318.html 24 | * E4H implementation - https://lists.w3.org/Archives/Public/public-script-coord/2013JanMar/0297.html 25 | * an AST implementation (such as E4H) would have to recreate an HTML parser to fully understand how incomplete tags are handled and their output such that `A B C` outputs `A B C` (this may have been true in 2008, but in 2016 chrome produces `A B C` which again, means trying to mimic an HTML parsers output would be constantly changing and difficult) - http://google-caja.googlecode.com/svn-history/r528/changes/mikesamuel/string-interpolation-29-Jan-2008/trunk/src/NOT-FOR-TRUNK/interp/index.html 26 | * description of context-aware auto-escape - https://js-quasis-libraries-and-repl.googlecode.com/svn/trunk/safetemplate.html#security_under_maintenance 27 | - https://developers.google.com/closure/templates/docs/security#in_urls 28 | - https://googleonlinesecurity.blogspot.com/2009/03/reducing-xss-by-way-of-automatic.html -------------------------------------------------------------------------------- /securitySpec.md: -------------------------------------------------------------------------------- 1 | # Preventative XSS measures 2 | 3 | ## XSS Prevention Rules Summary 4 | 5 | | Data Type | Context | Code Sample | Defense | 6 | | --------- | ------- | ----------- | ------- | 7 | | String | HTML Body |
    `UNTRUSTED DATA`
    | HTML Entity Encoding | 8 | | String | Safe HTML Attributes |
    ``
    |
    • Aggressive HTML Entity Encoding
    • Only place untrusted data into a whitelist of safe attributes (listed below).
    • Strictly validate unsafe attributes such as background, id and name.
    | 9 | | String | GET Parameter |
    `clickme`
    | URL Encoding | 10 | | String | Untrusted URL in a SRC or HREF attribute |
    `clickme`
    `