├── index.html ├── .gitignore ├── .travis.yml ├── page ├── xss.js └── index.html ├── karma.conf.js ├── gulpfile.js ├── bower.json ├── .jshintrc ├── package.json ├── karma.conf-ci.js ├── references.md ├── test ├── substitution.test.js ├── htmlPaser.test.js └── xss.test.js ├── securitySpec.md ├── README.md └── index.js /index.html: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .DS_Store 3 | coverage 4 | sauce_connect.log 5 | .env -------------------------------------------------------------------------------- /.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" -------------------------------------------------------------------------------- /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 | `); -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /page/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 20 | 21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /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']); -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /.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 | } -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | } -------------------------------------------------------------------------------- /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 -------------------------------------------------------------------------------- /test/substitution.test.js: -------------------------------------------------------------------------------- 1 | describe('Substitution expressions', function() { 2 | var min = 0, max = 99, disabled = true, heading = 1, tag = 'span'; 3 | 4 | it('should create a text node from a variable', function() { 5 | var el = html`${tag}`; 6 | 7 | // correct node 8 | expect(el.nodeType).to.equal(3); 9 | expect(el.nodeValue).to.equal('span'); 10 | 11 | // no extraneous side-effects 12 | expect(el.parentElement).to.be.null; 13 | }); 14 | 15 | it('should create a node from a variable', function() { 16 | var el = html`<${tag}>`; 17 | 18 | // correct node 19 | expect(el.nodeName).to.equal('SPAN'); 20 | 21 | // no extraneous side-effects 22 | expect(el.attributes.length, 'more than 1 attribute').to.equal(0); 23 | expect(el.children.length, 'more than 1 child').to.equal(0); 24 | expect(el.parentElement).to.be.null; 25 | expect(el.textContent).to.be.empty; 26 | }); 27 | 28 | it('should add attribute names or values from variables', function() { 29 | var el = html``; 30 | 31 | // correct node 32 | expect(el.nodeName).to.equal('INPUT'); 33 | 34 | // correct attributes 35 | expect(el.attributes.length).to.equal(7); 36 | expect(el.type).to.equal('number'); 37 | expect(el.min).to.equal('0'); 38 | expect(el.name).to.equal('number'); 39 | expect(el.id).to.equal('number'); 40 | expect(el.className).to.equal('number-input'); 41 | expect(el.disabled).to.equal(true); 42 | 43 | // no extraneous side-effects 44 | expect(el.children.length).to.equal(0); 45 | expect(el.parentElement).to.be.null; 46 | expect(el.textContent).to.be.empty; 47 | }); 48 | 49 | it('should skip empty attributes', function() { 50 | var emptyDisabled = false; 51 | var el = html``; 52 | 53 | // correct node 54 | expect(el.nodeName).to.equal('INPUT'); 55 | 56 | // correct attributes 57 | expect(el.attributes.length).to.equal(6); 58 | expect(el.type).to.equal('number'); 59 | expect(el.min).to.equal('0'); 60 | expect(el.name).to.equal('number'); 61 | expect(el.id).to.equal('number'); 62 | expect(el.className).to.equal('number-input'); 63 | expect(el.disabled).to.equal(false); 64 | 65 | // no extraneous side-effects 66 | expect(el.children.length).to.equal(0); 67 | expect(el.parentElement).to.be.null; 68 | expect(el.textContent).to.be.empty; 69 | }); 70 | 71 | it('should skip non-valid attribute substituted names', function() { 72 | var nonValidAttrName = []; 73 | var el = html`
`; 74 | 75 | // correct node 76 | expect(el.nodeName).to.equal('DIV'); 77 | 78 | // correct attributes 79 | expect(el.attributes.length).to.equal(0); 80 | 81 | // no extraneous side-effects 82 | expect(el.children.length).to.equal(0); 83 | expect(el.parentElement).to.be.null; 84 | expect(el.textContent).to.be.empty; 85 | }); 86 | 87 | it('should move any children from a substituted node to the new node', function() { 88 | var el = html`Hello`; 89 | 90 | // correct heading node 91 | expect(el.nodeName).to.equal('H1'); 92 | expect(el.attributes.length).to.equal(0); 93 | expect(el.children.length).to.equal(1); 94 | expect(el.parentElement).to.be.null; 95 | expect(el.textContent).to.equal('Hello'); 96 | 97 | // correct span node 98 | var span = el.firstChild; 99 | expect(span.nodeName).to.equal('SPAN'); 100 | expect(span.attributes.length, 'more than 1 attribute').to.equal(0); 101 | expect(span.children.length, 'more than 1 child').to.equal(0); 102 | expect(span.parentElement).to.equal(el); 103 | expect(span.textContent).to.equal('Hello'); 104 | }); 105 | 106 | it('should substitute in script tags', function() { 107 | var el = html``; 108 | 109 | // correct script node 110 | expect(el.nodeName).to.equal('SCRIPT'); 111 | expect(el.attributes.length).to.equal(0); 112 | expect(el.children.length).to.equal(0); 113 | expect(el.parentElement).to.be.null; 114 | expect(el.textContent).to.equal('x = 99'); 115 | }); 116 | 117 | it('should allow optional attributes', function() { 118 | var el = html``; 119 | 120 | expect(el.hasAttribute('disabled')).to.be.true; 121 | expect(el.getAttribute('disabled')).to.equal(''); 122 | 123 | var notDisabled = false; 124 | el = html``; 125 | 126 | expect(el.hasAttribute('disabled')).to.be.false; 127 | }); 128 | }); -------------------------------------------------------------------------------- /test/htmlPaser.test.js: -------------------------------------------------------------------------------- 1 | describe('HTML parser', function() { 2 | 3 | it('should return null for empty string', function() { 4 | var el = html``; 5 | 6 | expect(el).to.be.empty; 7 | }); 8 | 9 | it('should create a text node', function() { 10 | var el = html`foobar`; 11 | 12 | // correct node 13 | expect(el.nodeType).to.equal(3); 14 | expect(el.nodeValue).to.equal('foobar'); 15 | 16 | // no extraneous side-effects 17 | expect(el.parentElement).to.be.null; 18 | }); 19 | 20 | it('should create a single node', function() { 21 | var el = html``; 22 | 23 | // correct node 24 | expect(el.nodeName).to.equal('SPAN'); 25 | 26 | // no extraneous side-effects 27 | expect(el.attributes.length, 'more than 1 attribute').to.equal(0); 28 | expect(el.children.length, 'more than 1 child').to.equal(0); 29 | expect(el.parentElement).to.be.null; 30 | expect(el.textContent).to.be.empty; 31 | }); 32 | 33 | it('should create a single node when no closing tag is provided', function() { 34 | var el = html``; 35 | 36 | // correct node 37 | expect(el.nodeName).to.equal('SPAN'); 38 | 39 | // no extraneous side-effects 40 | expect(el.attributes.length, 'more than 1 attribute').to.equal(0); 41 | expect(el.children.length, 'more than 1 child').to.equal(0); 42 | expect(el.parentElement).to.be.null; 43 | expect(el.textContent).to.be.empty; 44 | }); 45 | 46 | it('should create a single node with attributes', function() { 47 | var el = html``; 48 | 49 | // correct node 50 | expect(el.nodeName).to.equal('INPUT'); 51 | 52 | // correct attributes 53 | expect(el.attributes.length).to.equal(7); 54 | expect(el.type).to.equal('number'); 55 | expect(el.min).to.equal('0'); 56 | expect(el.max).to.equal('99'); 57 | expect(el.name).to.equal('number'); 58 | expect(el.id).to.equal('number'); 59 | expect(el.className).to.equal('number-input'); 60 | expect(el.disabled).to.equal(true); 61 | 62 | // no extraneous side-effects 63 | expect(el.children.length).to.equal(0); 64 | expect(el.parentElement).to.be.null; 65 | expect(el.textContent).to.be.empty; 66 | }); 67 | 68 | it('should create a single node with children', function() { 69 | var el = html`
Hello
`; 70 | 71 | // correct container node 72 | expect(el.nodeName).to.equal('DIV'); 73 | expect(el.attributes.length).to.equal(1); 74 | expect(el.className).to.equal('container'); 75 | expect(el.children.length).to.equal(1); 76 | expect(el.parentElement).to.be.null; 77 | expect(el.textContent).to.equal('Hello'); 78 | 79 | // correct row node 80 | var row = el.firstChild; 81 | expect(row.nodeName).to.equal('DIV'); 82 | expect(row.attributes.length).to.equal(1); 83 | expect(row.className).to.equal('row'); 84 | expect(row.children.length).to.equal(1); 85 | expect(row.parentElement).to.equal(el); 86 | expect(row.textContent).to.equal('Hello'); 87 | 88 | // correct col node 89 | var col = row.firstChild; 90 | expect(col.nodeName).to.equal('DIV'); 91 | expect(col.attributes.length).to.equal(1); 92 | expect(col.className).to.equal('col'); 93 | expect(col.children.length).to.equal(1); 94 | expect(col.parentElement).to.equal(row); 95 | expect(col.textContent).to.equal('Hello'); 96 | 97 | // correct leaf node 98 | var leaf = col.firstChild; 99 | expect(leaf.nodeName).to.equal('DIV'); 100 | expect(leaf.attributes.length).to.equal(0); 101 | expect(leaf.children.length).to.equal(0); 102 | expect(leaf.parentElement).to.equal(col); 103 | expect(leaf.textContent).to.equal('Hello'); 104 | }); 105 | 106 | it('should create sibling nodes', function() { 107 | var nodes = html``; 108 | 109 | // correct node 110 | expect(nodes).to.be.instanceof(DocumentFragment); 111 | expect(nodes.childNodes.length).to.equal(2); 112 | 113 | // correct first child 114 | var tr = nodes.querySelectorAll('tr')[0]; 115 | expect(tr.nodeName).to.equal('TR'); 116 | expect(tr.attributes.length, 'more than 1 attribute').to.equal(0); 117 | expect(tr.children.length, 'more than 1 child').to.equal(0); 118 | expect(tr.parentElement).to.be.null; 119 | expect(tr.textContent).to.be.empty; 120 | 121 | // correct second child 122 | var tr2 = nodes.querySelectorAll('tr')[1]; 123 | expect(tr2.nodeName).to.equal('TR'); 124 | expect(tr2.attributes.length, 'more than 1 attribute').to.equal(0); 125 | expect(tr2.children.length, 'more than 1 child').to.equal(0); 126 | expect(tr2.parentElement).to.be.null; 127 | expect(tr2.textContent).to.be.empty; 128 | }); 129 | 130 | it('should execute a script tag', function() { 131 | var el = html``; 132 | document.body.appendChild(el); 133 | 134 | // correct node 135 | expect(el.nodeName).to.equal('SCRIPT'); 136 | expect(el.attributes.length).to.equal(0); 137 | expect(el.children.length).to.equal(0); 138 | expect(el.textContent).to.equal('foo = "bar";'); 139 | 140 | // script was executed 141 | expect(foo).to.equal('bar'); 142 | }); 143 | }); -------------------------------------------------------------------------------- /test/xss.test.js: -------------------------------------------------------------------------------- 1 | var counter = 0; 2 | 3 | describe('XSS Attack Vectors', function() { 4 | // Modified XSS String 5 | // (Source: https://developers.google.com/closure/templates/docs/security#example) 6 | var xss = "javascript:/*/**/ /"; 7 | 8 | afterEach(function() { 9 | counter++; 10 | }); 11 | 12 | it('should prevent injection to element innerHTML', function() { 13 | var el = html`

${xss}

`; 14 | document.body.appendChild(el); 15 | }); 16 | 17 | it('should prevent injection to non-quoted element attributes', function() { 18 | var el = html`
`; 19 | document.body.appendChild(el); 20 | }); 21 | 22 | it('should prevent injection to single quoted element attributes', function() { 23 | var el = html`
`; 24 | document.body.appendChild(el); 25 | }); 26 | 27 | it('should prevent injection to double quoted element attributes', function() { 28 | var el = html`
`; 29 | document.body.appendChild(el); 30 | }); 31 | 32 | it('should prevent injection as a javascript quoted string', function() { 33 | var el = html``; 34 | document.body.appendChild(el); 35 | }); 36 | 37 | it('should prevent injection on one side of a javascript quoted expression', function() { 38 | var el = html``; 39 | document.body.appendChild(el); 40 | }); 41 | 42 | it('should prevent injection into inlined quoted event handler', function() { 43 | var el = html`XSS <p> tag`; 44 | document.body.appendChild(el); 45 | el.click(); 46 | }); 47 | 48 | it('should prevent injection into quoted event handler', function() { 49 | var el = html`XSS <p> tag`; 50 | document.body.appendChild(el); 51 | el.click(); 52 | }); 53 | 54 | it('should prevent injection into CSS unquote property', function() { 55 | var el = html``; 56 | document.body.appendChild(el); 57 | }); 58 | 59 | it('should prevent injection into CSS quoted property', function() { 60 | var el = html``; 61 | document.body.appendChild(el); 62 | }); 63 | 64 | it('should prevent injection into CSS property of HTML style attribute', function() { 65 | var el = html`
`; 66 | document.body.appendChild(el); 67 | }); 68 | 69 | it('should prevent injection into query params of HTML urls', function() { 70 | var el = html`

XSS'ed Link

`; 71 | document.body.appendChild(el); 72 | el.click(); 73 | }); 74 | 75 | it('should prevent injection into HREF attribute of tag', function() { 76 | var el = html`XSS'ed Link`; 77 | document.body.appendChild(el); 78 | el.click(); 79 | }); 80 | 81 | it('should prevent against clobbering of /attributes/', function() { 82 | var el = html`
83 | 84 | 85 |
`; 86 | document.body.appendChild(el); 87 | 88 | // el.submit() does not trigger a submit event, so we need to click the submit button 89 | // @see http://stackoverflow.com/questions/11557994/jquery-submit-vs-javascript-submit 90 | el.querySelector('input[type="submit"]').click(); 91 | }); 92 | 93 | it('should prevent injection out of a tag name by throwing an error', function() { 94 | var func = function() { 95 | var el = html``; 96 | document.body.appendChild(el); 97 | }; 98 | 99 | expect(func).to.throw; 100 | }); 101 | 102 | it('should prevent xss protocol URLs by rejecting them', function() { 103 | var el = html``; 104 | document.body.appendChild(el); 105 | el.click(); 106 | 107 | expect(el.getAttribute('href')[0]).to.equal('#'); 108 | }); 109 | 110 | it('should not prevent javascript protocol if it was a safe string', function() { 111 | var value = 'foo/bar&baz/boo'; 112 | var el = html``; 113 | 114 | expect(el.getAttribute('href')).to.equal('javascript:void(0);'); 115 | }); 116 | 117 | it('should prevent injection into uri custom attributes', function() { 118 | var el = html`` 119 | document.body.appendChild(el); 120 | el.href = el.getAttribute('data-uri'); 121 | el.click(); 122 | }); 123 | 124 | it('should entity escape URLs', function() { 125 | var value = 'foo/bar&baz/boo'; 126 | var el = html``; 127 | 128 | expect(el.getAttribute('href')).to.equal('foo/bar&baz/boo'); 129 | }); 130 | 131 | it('should percent encode inside URL query', function() { 132 | var value = 'bar&baz=boo'; 133 | var el = html``; 134 | 135 | expect(el.getAttribute('href')).to.equal('foo?q=bar%26baz%3Dboo'); 136 | }); 137 | 138 | it('should percent encode inside URL query and entity escape if not', function() { 139 | var value = 'bar&baz=boo'; 140 | var el = html``; 141 | 142 | expect(el.getAttribute('href')).to.equal('foo/bar&baz=boo/bar?q=bar%26baz%3Dboo'); 143 | }); 144 | 145 | it('should reject a URL outright if it has the wrong protocol', function() { 146 | var protocol = 'javascript:alert(1337)'; 147 | var value = '/foo&bar/bar'; 148 | var el = html``; 149 | 150 | expect(el.getAttribute('href')[0]).to.equal('#'); 151 | expect(el.getAttribute('href').indexOf('/bar')).to.equal(-1); 152 | }); 153 | 154 | it('should allow a URL if it has a safe protocol', function() { 155 | var protocol = 'http://localhost:500'; 156 | var value = '/foo?id=true'; 157 | var el = html``; 158 | 159 | expect(el.getAttribute('href')).to.equal('http://localhost:500/bar/foo?id=true'); 160 | 161 | var protocol = 'https://localhost:500'; 162 | var el = html``; 163 | 164 | expect(el.getAttribute('href')).to.equal('https://localhost:500/bar/foo?id=true'); 165 | }); 166 | 167 | }); 168 | -------------------------------------------------------------------------------- /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`
`