├── .eslintrc.json ├── .gitignore ├── .travis.yml ├── LICENSE.md ├── README.md ├── docs └── rules │ ├── no-location-href-assign.md │ └── no-mixed-html.md ├── lib ├── Rules.js ├── index.js ├── re.js ├── rules │ ├── no-location-href-assign.js │ └── no-mixed-html.js └── tree.js ├── package.json └── tests └── lib └── rules ├── no-location-href-assign.js └── no-mixed-html.js /.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 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 ? '
    ' : '