├── .gitignore ├── .travis.yml ├── lib ├── re.js ├── index.js ├── Rules.js ├── rules │ ├── no-location-href-assign.js │ └── no-mixed-html.js └── tree.js ├── package.json ├── LICENSE.md ├── docs └── rules │ ├── no-location-href-assign.md │ └── no-mixed-html.md ├── README.md ├── .eslintrc.json └── tests └── lib └── rules ├── no-location-href-assign.js └── no-mixed-html.js /.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules 2 | *.swp 3 | *.log 4 | .idea 5 | package-lock.json 6 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | install: 3 | - npm install -g istanbul 4 | - npm install -g codeclimate-test-reporter 5 | - npm install 6 | - npm install codecov.io 7 | node_js: 8 | - 10 9 | script: 10 | - npm test 11 | - istanbul cover node_modules/mocha/bin/_mocha -- tests --recursive 12 | after_script: 13 | - codeclimate < coverage/lcov.info 14 | - node_modules/codecov.io/bin/codecov.io.js < coverage/coverage.json 15 | -------------------------------------------------------------------------------- /lib/re.js: -------------------------------------------------------------------------------- 1 | 2 | 'use strict'; 3 | 4 | module.exports = { 5 | 6 | toRegexp: function( str ) { 7 | var pair = str.split( '/' ); 8 | return new RegExp( pair[ 0 ], pair[ 1 ] ); 9 | }, 10 | 11 | any: function( input, regexps ) { 12 | 13 | for( var i = 0; i < regexps.length; i++ ) { 14 | if( regexps[ i ].exec( input ) ) 15 | return true; 16 | } 17 | 18 | return false; 19 | }, 20 | }; 21 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "eslint-plugin-xss", 3 | "version": "0.1.12", 4 | "description": "Validates XSS related issues of mixing HTML and non-HTML content in variables.", 5 | "keywords": [ 6 | "eslint", 7 | "eslintplugin", 8 | "eslint-plugin" 9 | ], 10 | "author": "Mikko Rantanen", 11 | "repository": "https://github.com/Rantanen/eslint-plugin-xss", 12 | "main": "lib/index.js", 13 | "scripts": { 14 | "test": "mocha tests --recursive", 15 | "lint": "eslint ./lib ./tests/" 16 | }, 17 | "dependencies": { 18 | "requireindex": "~1.1.0" 19 | }, 20 | "devDependencies": { 21 | "eslint": "~2.6.0", 22 | "mocha": "^7.0.1", 23 | "mochawesome": "^4.1.0" 24 | }, 25 | "engines": { 26 | "node": ">=0.10.0" 27 | }, 28 | "license": "ISC" 29 | } 30 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Internet Systems Consortium license 2 | =================================== 3 | 4 | Copyright (c) `2016`, `Mikko Rantanen` 5 | 6 | Permission to use, copy, modify, and/or distribute this software for any purpose 7 | with or without fee is hereby granted, provided that the above copyright notice 8 | and this permission notice appear in all copies. 9 | 10 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH 11 | REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND 12 | FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, 13 | INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS 14 | OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER 15 | TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF 16 | THIS SOFTWARE. 17 | -------------------------------------------------------------------------------- /lib/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @fileoverview Validates M-Files coding conventions 3 | * @author Mikko Rantanen 4 | */ 5 | 'use strict'; 6 | 7 | // ----------------------------------------------------------------------------- 8 | // Requirements 9 | // ----------------------------------------------------------------------------- 10 | 11 | var requireIndex = require( 'requireindex' ); 12 | 13 | // ----------------------------------------------------------------------------- 14 | // Plugin Definition 15 | // ----------------------------------------------------------------------------- 16 | 17 | module.exports = { 18 | // import all rules in lib/rules 19 | rules: requireIndex(__dirname + '/rules'), 20 | // allow users to extend the recommended configurations 21 | configs: { 22 | recommended: { 23 | plugins: ['xss'], 24 | rules: { 25 | 'xss/no-mixed-html': 'error', 26 | 'xss/no-location-href-assign': 'error', 27 | }, 28 | }, 29 | }, 30 | }; 31 | -------------------------------------------------------------------------------- /lib/Rules.js: -------------------------------------------------------------------------------- 1 | 2 | 'use strict'; 3 | 4 | var tree = require( './tree' ); 5 | 6 | /** 7 | * Rule library for different nodes. 8 | * 9 | * @param {{ functionRules: Object }} options - Rule options. 10 | */ 11 | var Rules = function( options ) { 12 | 13 | this.data = {}; 14 | this._addCategory( 'functions', options.functionRules ); 15 | }; 16 | 17 | 18 | /** 19 | * Gets the node context rules. 20 | * 21 | * @param {Node} node - Node to get the rules for 22 | * 23 | * @returns {Object} Context rules or an empty rule object. 24 | */ 25 | Rules.prototype.get = function( node ) { 26 | if( node.type === 'CallExpression' ) 27 | return this.getFunctionRules( node ); 28 | 29 | return {}; 30 | }; 31 | 32 | /** 33 | * Gets the node context rules. 34 | * 35 | * @param {Node} node - Node to get the rules for 36 | * 37 | * @returns {Object} Context rules or an empty rule object. 38 | */ 39 | Rules.prototype.getFunctionRules = function( node ) { 40 | return this._getWithCache( node, this.data.functions ); 41 | }; 42 | 43 | Rules.prototype._getWithCache = function( node, data ) { 44 | 45 | var fullName = tree.getFullItemName( node ); 46 | if( data.cache[ fullName ] ) 47 | return data.cache[ fullName ]; 48 | 49 | var rules = null; 50 | var partialNames = tree.getRuleNames( node ); 51 | for( var i = 0; !rules && i < partialNames.length; i++ ) { 52 | rules = data.rules[ partialNames[ i ] ]; 53 | } 54 | 55 | return data.cache[ fullName ] = rules || {}; 56 | }; 57 | 58 | Rules.prototype._addCategory = function( type, rules ) { 59 | this.data[ type ] = { 60 | cache: Object.create( null ), 61 | rules: rules || {} 62 | }; 63 | }; 64 | 65 | module.exports = Rules; 66 | 67 | -------------------------------------------------------------------------------- /docs/rules/no-location-href-assign.md: -------------------------------------------------------------------------------- 1 | # Checks for all assignments to location.href 2 | 3 | This rule ensures that you are calling escape logic before assigning to location.href property. 4 | 5 | ## Rule Details 6 | 7 | This rule tries to prevent XSS that can be created by assigning some user input directly to 8 | location.href property. Here is an example of how we can execute any js code in that way; 9 | 10 | ```js 11 | window.location.href = 'javascript:alert("xss")' 12 | ``` 13 | 14 | 15 | The following patterns are considered as errors: 16 | 17 | ```js 18 | 19 | window.location.href = 'some evil user content'; 20 | document.location.href = 'some evil user content'; 21 | location.href = 'some evil user content'; 22 | location.href = getNextUrl(); 23 | 24 | ``` 25 | 26 | The following patterns are not errors: 27 | 28 | ```js 29 | // this rule ensures that you are calling escape function before location.href assignment 30 | // 'escape' name can be configured via options. 31 | location.href = escape('some evil url'); 32 | 33 | ``` 34 | The concrete implementation of escape is up to you and how you will decide to escape location.href value. This rule 35 | only ensures that you are handling assignment in a proper way (by wrapping the right part with the escape function). 36 | 37 | ### Options 38 | 39 | ```js 40 | "xss/no-location-href-assign": [ 2, { 41 | "escapeFunc": "escapeHref" 42 | } ]; 43 | ``` 44 | 45 | ### escapeFunc (optional) 46 | Function name that is used to sanitize user input. 'escape' is used by default. 47 | 48 | 49 | ## When Not To Use It 50 | 51 | When you are running your code outside of browser environment (node) or you don't care about XSS vulnerabilities. 52 | 53 | ## Further Reading 54 | 55 | - [XSS Prevention CHeat Sheet - OWASP](https://cheatsheetseries.owasp.org/cheatsheets/Cross_Site_Scripting_Prevention_Cheat_Sheet.html) 56 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # eslint-plugin-xss 2 | 3 | [![NPM version](http://img.shields.io/npm/v/eslint-plugin-xss.svg)](https://www.npmjs.com/package/eslint-plugin-xss) 4 | [![Build Status](https://travis-ci.org/Rantanen/eslint-plugin-xss.svg?branch=master)](https://travis-ci.org/Rantanen/eslint-plugin-xss) 5 | [![Codecov](https://codecov.io/gh/Rantanen/eslint-plugin-xss/branch/master/graph/badge.svg)](https://codecov.io/gh/Rantanen/eslint-plugin-xss) 6 | [![Codacy](https://api.codacy.com/project/badge/grade/13e5c7abeb4545359ca9b02c0e91bb72)](https://www.codacy.com/app/jubjub/eslint-plugin-xss) 7 | 8 | Tries to detect XSS issues in codebase before they end up in production. 9 | 10 | ## Installation 11 | 12 | You'll first need to install [ESLint](http://eslint.org): 13 | 14 | ``` 15 | $ npm install eslint --save-dev 16 | ``` 17 | 18 | Next, install `eslint-plugin-xss`: 19 | 20 | ``` 21 | $ npm install eslint-plugin-xss --save-dev 22 | ``` 23 | 24 | **Note:** If you installed ESLint globally (using the `-g` flag) then you must also install `eslint-plugin-xss` globally. 25 | 26 | ## Usage 27 | 28 | Add `xss` to the plugins section of your `.eslintrc` configuration file. You can omit the `eslint-plugin-` prefix: 29 | 30 | ```json 31 | { 32 | "plugins": [ 33 | "xss" 34 | ] 35 | } 36 | ``` 37 | 38 | Then configure the rules you want to use under the rules section. 39 | 40 | ```json 41 | { 42 | "rules": { 43 | "xss/rule-name": 2 44 | } 45 | } 46 | ``` 47 | 48 | Or: 49 | 50 | Enable all rules by adding the following to your `.eslintrc` configuration file 51 | 52 | ```json 53 | { 54 | "extends": [ 55 | "plugin:xss/recommended" 56 | ] 57 | } 58 | ``` 59 | 60 | ## Supported Rules 61 | 62 | * [xss/no-mixed-html](docs/rules/no-mixed-html.md): Warn about possible XSS issues. 63 | * [xss/no-location-href-assign](docs/rules/no-location-href-assign.md): Warn when trying to modify location.href. 64 | 65 | -------------------------------------------------------------------------------- /lib/rules/no-location-href-assign.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @fileoverview prevents xss by assignment to location href javascript url string 3 | * @author Alexander Mostovenko 4 | */ 5 | 'use strict'; 6 | 7 | 8 | // ------------------------------------------------------------------------------ 9 | // Plugin Definition 10 | // ------------------------------------------------------------------------------ 11 | 12 | var ERROR = 'Dangerous location.href assignment can lead to XSS'; 13 | 14 | module.exports = { 15 | meta: { 16 | docs: { 17 | description: 'disallow location.href assignment (prevent possible XSS)' 18 | }, 19 | }, 20 | create: function( context ) { 21 | var escapeFunc = context.options[ 0 ] && 22 | context.options[ 0 ].escapeFunc || 'escape'; 23 | 24 | return { 25 | AssignmentExpression: function( node ) { 26 | var left = node.left; 27 | var isHref = left.property && left.property.name === 'href'; 28 | if( !isHref ) { 29 | return; 30 | } 31 | var isLocationObject = left.object && left.object.name === 'location'; 32 | var isLocationProperty = left.object.property && 33 | left.object.property.name === 'location'; 34 | 35 | if( !( isLocationObject || isLocationProperty ) ) { 36 | return; 37 | } 38 | 39 | var sourceCode = context.getSourceCode(); 40 | if( node.right.callee && ( node.right.callee.name === escapeFunc || sourceCode.getText( node.right.callee ) === escapeFunc ) ) { 41 | return; 42 | } 43 | var rightSource = sourceCode.getText( node.right ); 44 | var errorMsg = ERROR + 45 | '. Please use ' + escapeFunc + 46 | '(' + rightSource + ') as a wrapper for escaping'; 47 | 48 | context.report( { node: node, message: errorMsg } ); 49 | } 50 | }; 51 | } 52 | }; 53 | 54 | 55 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "eslint:recommended", 3 | "parserOptions": { 4 | "ecmaVersion": 6, 5 | "sourceType": "script", 6 | "ecmaFeatures": { } 7 | }, 8 | "env": { 9 | "node": true, 10 | "browser": true, 11 | "es6": true 12 | }, 13 | "rules": { 14 | "no-console": 0, 15 | "callback-return": [ "error", [ "done", "cb", "next" ] ], 16 | 17 | "no-unused-vars": [ "error", { "vars": "all", "args": "none" } ], 18 | "comma-dangle": 0, 19 | "strict": [ "error", "global" ], 20 | "eqeqeq": 2, 21 | 22 | "array-bracket-spacing": [ "error", "always" ], 23 | "block-spacing": [ "error", "always" ], 24 | "brace-style": [ "error", "1tbs", { "allowSingleLine": true } ], 25 | "camelcase": 2, 26 | "comma-spacing": [ "error", { "before": false, "after": true } ], 27 | "comma-style": 2, 28 | "computed-property-spacing": [ "error", "always" ], 29 | "consistent-this": [ "error", "self" ], 30 | "func-style": [ "error", "expression" ], 31 | "key-spacing": [ "error", { "beforeColon": false, "afterColon": true } ], 32 | "keyword-spacing": [ "error", { 33 | "after": false, 34 | "overrides": { 35 | "from": { "after": true }, 36 | "return": { "after": true }, 37 | "import": { "after": true }, 38 | "else": { "after": true }, 39 | "try": { "after": true }, 40 | "do": { "after": true } 41 | } 42 | } ], 43 | "lines-around-comment": [ "error", { 44 | "beforeLineComment": true, 45 | "beforeBlockComment": true, 46 | "allowArrayStart": true, 47 | "allowObjectStart": true 48 | } ], 49 | 50 | "new-cap": 2, 51 | "no-lonely-if": 1, 52 | "no-mixed-spaces-and-tabs": 2, 53 | "no-multiple-empty-lines": [ "error", { "max": 2 }], 54 | "no-trailing-spaces": 2, 55 | "no-unneeded-ternary": 1, 56 | "no-whitespace-before-property": 2, 57 | "object-curly-spacing": [ "error", "always" ], 58 | "semi": 2, 59 | "semi-spacing": [ "error", { "before": false, "after": true } ], 60 | "space-before-blocks": [ "error", "always" ], 61 | "space-before-function-paren": [ "error", "never" ], 62 | "space-in-parens": [ "error", "always" ], 63 | "space-infix-ops": [ "error", { "int32Hint": false } ], 64 | "spaced-comment": [ "error", "always" ], 65 | "valid-jsdoc": [ "error", { 66 | "prefer": { 67 | "return": "returns" 68 | }, 69 | "requireReturn": false, 70 | "matchDescription": "^[A-Z](\n|.)*[.](\n|$)", 71 | "preferType": { 72 | "String": "string", 73 | "Number": "number", 74 | "object": "Object" 75 | } 76 | } ], 77 | 78 | "space-unary-ops": [ "error", { 79 | "words": true, 80 | "nonwords": false 81 | // Company style requires "!" to be followed by space. 82 | // "overrides": { "!": true } 83 | } ], 84 | 85 | // Options incompatible with company style. 86 | "indent": [ "error", 4 ], 87 | "max-len": [ "error", 90 ], 88 | "quotes": [ "error", "single" ] 89 | } 90 | } 91 | 92 | -------------------------------------------------------------------------------- /tests/lib/rules/no-location-href-assign.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @fileoverview checks rule that prevents xss by assignment 3 | * to location href javascript url string 4 | * @author Alexander Mostovenko 5 | */ 6 | 'use strict'; 7 | 8 | var rule = require( '../../../lib/rules/no-location-href-assign' ), 9 | RuleTester = require( 'eslint' ).RuleTester; 10 | 11 | var ruleTester = new RuleTester(); 12 | ruleTester.run( 'no-location-href-assign', rule, { 13 | 14 | valid: [ 15 | 'someLink.href = \'www\'', 16 | 'href = \'wwww\'', 17 | { 18 | code: 'location.href = escape(\'www\')', 19 | options: [ { escapeFunc: 'escape' } ] 20 | }, 21 | { 22 | code: 'location.href = DOMPurify.sanitize(\'www\')', 23 | options: [ { escapeFunc: 'DOMPurify.sanitize' } ] 24 | } 25 | ], 26 | 27 | invalid: [ 28 | { 29 | code: 'location.href = wrapper(escape(\'www\'))', 30 | options: [ { escapeFunc: 'escapeXSS' } ], 31 | errors: [ { 32 | message: 'Dangerous location.href assignment can lead to XSS.' + 33 | ' Please use escapeXSS(wrapper(escape(\'www\'))) ' + 34 | 'as a wrapper for escaping' 35 | } ] 36 | }, 37 | { 38 | code: 'location.href = wrapper(\'www\')', 39 | options: [ { escapeFunc: 'escape' } ], 40 | errors: [ { 41 | message: 'Dangerous location.href assignment can lead to XSS.' + 42 | ' Please use escape(wrapper(\'www\')) as a wrapper for escaping' 43 | } ] 44 | }, 45 | { 46 | code: 'location.href = \'some location\'', 47 | errors: [ { 48 | message: 'Dangerous location.href assignment can lead to XSS.' + 49 | ' Please use escape(\'some location\') as a wrapper for escaping' 50 | } ] 51 | }, 52 | { 53 | code: 'location.href = \'some location for memberExpression callee\'', 54 | options: [ { escapeFunc: 'DOMPurify.sanitize' } ], 55 | errors: [ { 56 | message: 'Dangerous location.href assignment can lead to XSS.' + 57 | ' Please use DOMPurify.sanitize(\'some location for memberExpression callee\') as a wrapper for escaping' 58 | } ] 59 | }, 60 | { 61 | code: 'window.location.href = \'some location\'', 62 | errors: [ { 63 | message: 'Dangerous location.href assignment can lead to XSS.' + 64 | ' Please use escape(\'some location\') as a wrapper for escaping' 65 | } ] 66 | }, 67 | { 68 | code: 'document.location.href = \'some location\'', 69 | errors: [ { 70 | message: 'Dangerous location.href assignment can lead to XSS.' + 71 | ' Please use escape(\'some location\') as a wrapper for escaping' 72 | } ] 73 | }, 74 | { 75 | code: 'window.document.location.href = \'some location\'', 76 | errors: [ { 77 | message: 'Dangerous location.href assignment can lead to XSS.' + 78 | ' Please use escape(\'some location\') as a wrapper for escaping' 79 | } ] 80 | }, 81 | { 82 | code: 'window.document.location.href = getNextUrl()', 83 | errors: [ { 84 | message: 'Dangerous location.href assignment can lead to XSS.' + 85 | ' Please use escape(getNextUrl()) as a wrapper for escaping' 86 | } ] 87 | } 88 | ] 89 | } ); 90 | -------------------------------------------------------------------------------- /docs/rules/no-mixed-html.md: -------------------------------------------------------------------------------- 1 | # Checks for missing encoding when concatenating HTML strings (require-encode) 2 | 3 | Wanted a way to catch XSS issues in code before they end up in production. 4 | 5 | ## Rule Details 6 | 7 | This rule aims to catch as many XSS issues by examining the code as possible. 8 | The rule checks for mixed html/non-html content, unescaped input, etc. 9 | 10 | The following patterns are considered warnings: 11 | 12 | ```js 13 | 14 | // Mixed content 15 | var x = '
' + input + '
'; 16 | $node.html( '
' + input + '
' ); 17 | 18 | // Unsafe container names. 19 | var html = input; 20 | var text = htmlInput; 21 | displayValue( htmlInput ); 22 | 23 | // Checking certain expression parameters that might end up in the variables. 24 | var htmlItems = [ input1, input2 ].join(); 25 | var textItems = [ '
', input, '
' ].join(); 26 | var tag = isNumbered ? '
    ' : '