├── .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 | [](https://www.npmjs.com/package/eslint-plugin-xss)
4 | [](https://travis-ci.org/Rantanen/eslint-plugin-xss)
5 | [](https://codecov.io/gh/Rantanen/eslint-plugin-xss)
6 | [](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 ? '' : '';
27 |
28 | // Checking function return values.
29 | var createHtml = function( item ) { return item.name; }
30 | var createBox = function( item ) { return '' + encode( item ) + '
' }
31 |
32 | ```
33 |
34 | The following patterns are not warnings:
35 |
36 | ```js
37 |
38 | // Proper encoding
39 | var html = '' + encode( input ) + '
';
40 | $node.html( '' + encode( input ) + '
' );
41 |
42 | // Proper container names
43 | var html = '
';
44 | var text = textbox.value;
45 |
46 | ```
47 |
48 | ### Options
49 |
50 | ```js
51 | "xss/no-mixed-html": [ 2, {
52 | "htmlVariableRules": [ "AsHtml", "HtmlEncoded/i", "^html$" ],
53 | "htmlFunctionRules": [ ".asHtml/i", "toHtml" ],
54 | "functions": {
55 | "$": {
56 | "htmlInput": true,
57 | "safe": [ "document", "this" ]
58 | },
59 | ".html": {
60 | "htmlInput": true,
61 | "htmlOutput": true
62 | },
63 | ".join": {
64 | "passthrough": { "obj": true, "args": true }
65 | }
66 | }
67 | } ];
68 | ```
69 |
70 | #### `htmlVariableRules`, `htmlFunctionRules`
71 |
72 | `htmlVariableRules` and `htmlFunctionRules` specify the naming convention used
73 | for storing HTML variables and defining functions returning HTML values. Both
74 | of these options are defined as Regex-arrays. The regex options, such as case
75 | insensitive matching can be defined with a delimiting '/'.
76 |
77 | ##### Examples
78 |
79 | ```js
80 | "htmlVariableRules": [
81 | "AsHtml", // Matches fooAsHtml, barAsHtmlValue. Doesn't match: myHTML.
82 | "HtmlEncoded/i", // Matches htmlEncoded, HTMLEncodedValue
83 | "^html$", // Matches html
84 | ],
85 | "htmlFunctionRules": [
86 | ".asHtml/i", // Matches foo.asHTML(), doesn't match asHtml()
87 | "toHtml", // Matches toHtml( txt ), doesn't match value.toHtml()
88 | ]
89 | ````
90 |
91 | #### `functions`
92 |
93 | `functions` specify special rules for certain functions.
94 |
95 | - `htmlInput` makes the function require HTML input. By default calling a
96 | function with HTML input will yield a warning. Specifying `htmlInput` option
97 | on the function will yield a warning when the function is used with unencoded
98 | input.
99 | - Functions with `htmlOutput` defined are considered to return HTML output.
100 | Mixing this with unencoded values or storing it in non-HTML variables will
101 | yield a warning. Also functions that match `htmlFunctionRules` are considered
102 | to return HTML output in the same way.
103 | - `passthrough` defines which parameters of a function are passed through as
104 | such. This can be used to specify functions like `Array.join`. Passthrough
105 | functions won't perform any checking on their own, instead the checks depend
106 | on where the parameter values are used in.
107 | - `safe` can be used to disable warnings for certain parameters for the
108 | functions. The jQuery function `$()` is often used with HTML or CSS input,
109 | which should be encoded - but it may also be used to construct jQuery-lists
110 | from DOM elements - the two most common usages being: `$( document )` and `$(
111 | this )`. The value should be either `true`/`false` for blanket safety or an
112 | array of accepted variable names.
113 |
114 |
115 | ## When Not To Use It
116 |
117 | If you are creating a Node.js application that doesn't output any HTML, you can
118 | safely disable this rule.
119 |
120 | ## Further Reading
121 |
122 | - [XSS Prevention CHeat Sheet - OWASP](https://cheatsheetseries.owasp.org/cheatsheets/Cross_Site_Scripting_Prevention_Cheat_Sheet.html)
123 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/lib/rules/no-mixed-html.js:
--------------------------------------------------------------------------------
1 | /**
2 | * @fileoverview Checks for missing encoding when concatenating HTML strings
3 | * @author Mikko Rantanen
4 | */
5 | 'use strict';
6 |
7 | var re = require( '../re' );
8 | var tree = require( '../tree' );
9 | var Rules = require( '../Rules' );
10 |
11 | // -----------------------------------------------------------------------------
12 | // Rule Definition
13 | // -----------------------------------------------------------------------------
14 |
15 | module.exports = function( context ) {
16 |
17 | // Default options.
18 | var htmlVariableRules = [ 'html/i' ];
19 | var htmlFunctionRules = [ 'AsHtml' ];
20 | var functionRules = {
21 | '.join': { passthrough: { obj: true, args: true } },
22 | '.toString': { passthrough: { obj: true } },
23 | '.substr': { passthrough: { obj: true } },
24 | '.substring': { passthrough: { obj: true } },
25 | };
26 |
27 | // Read the user specified options.
28 | if( context.options.length > 0 ) {
29 | var opts = context.options[ 0 ];
30 |
31 | htmlVariableRules = opts.htmlVariableRules || htmlVariableRules;
32 | htmlFunctionRules = opts.htmlFunctionRules || htmlFunctionRules;
33 | functionRules = opts.functions || functionRules;
34 | }
35 |
36 | // Turn the name rules from string/string array to regexp.
37 | htmlVariableRules = htmlVariableRules.map( re.toRegexp );
38 | htmlFunctionRules = htmlFunctionRules.map( re.toRegexp );
39 |
40 | var allRules = new Rules( {
41 | functionRules: functionRules
42 | } );
43 |
44 | // Expression stack for tracking the topmost expression that is marked
45 | // XSS-candidate when we find '' strings.
46 | var exprStack = [];
47 |
48 | // -------------------------------------------------------------------------
49 | // Helpers
50 | // -------------------------------------------------------------------------
51 |
52 |
53 | /**
54 | * Checks whether the node represents a passthrough function.
55 | *
56 | * @param {Node} node - Node to check.
57 | *
58 | * @returns {bool} - True, if the node is an array join.
59 | */
60 | var getPassthrough = function( node ) {
61 |
62 | if( node.type !== 'CallExpression' )
63 | return false;
64 |
65 | var rules = allRules.getFunctionRules( node );
66 | return rules.passthrough;
67 | };
68 |
69 | /**
70 | * Gets all descendants that we know to affect the possible output string.
71 | *
72 | * @param {Node} node - Node for which to get the descendants. Inclusive.
73 | * @param {Node} _children - Collection of descendants. Leave null.
74 | * @param {Node} _hasRecursed -
75 | * Defines whether the function has recursed into inner structures.
76 | * Leave false.
77 | *
78 | * @returns {Node[]} - Flat list of descendant nodes.
79 | */
80 | var getDescendants = function( node, _children, _hasRecursed ) {
81 |
82 | // The children array may be passed during recursion.
83 | if( _children === undefined )
84 | _children = [];
85 |
86 | // Handle the special case of .join() function.
87 | var passthrough = getPassthrough( node );
88 | if( passthrough ) {
89 |
90 | // Get the descedants from the array and the function argument.
91 | if( passthrough.obj ) {
92 | getDescendants( node.callee.object, _children, _hasRecursed );
93 | }
94 |
95 | if( passthrough.args ) {
96 | node.arguments.forEach( function( a ) {
97 | getDescendants( a, _children, _hasRecursed );
98 | } );
99 | }
100 |
101 | return _children;
102 | }
103 |
104 | // Check the expression type.
105 | if( node.type === 'CallExpression' ||
106 | node.type === 'NewExpression' ||
107 | node.type === 'ThisExpression' ||
108 | node.type === 'ObjectExpression' ||
109 | node.type === 'FunctionExpression' ||
110 | node.type === 'UnaryExpression' ||
111 | node.type === 'UpdateExpression' ||
112 | node.type === 'MemberExpression' ||
113 | node.type === 'SequenceExpression' ||
114 | node.type === 'Literal' ||
115 | node.type === 'Identifier' ||
116 | ( _hasRecursed && node.type === 'ArrayExpression' )
117 | ) {
118 |
119 | // Basic expressions that won't be reflected further.
120 | _children.push( node );
121 |
122 | } else if( node.type === 'ArrayExpression' ) {
123 |
124 | // For array nodes, get the descendant nodes.
125 | node.elements.forEach( function( e ) {
126 | getDescendants( e, _children, true );
127 | } );
128 |
129 | } else if( node.type === 'BinaryExpression' ) {
130 |
131 | // Binary expressions concatenate strings.
132 | //
133 | // Recurse to both left and right side.
134 | getDescendants( node.left, _children, true );
135 | getDescendants( node.right, _children, true );
136 |
137 | } else if( node.type === 'AssignmentExpression' ) {
138 |
139 | // There might be assignment expressions in the middle of the node.
140 | // Use the assignment identifier as the descendant.
141 | //
142 | // The assignment itself will be checked with its own descendants
143 | // check.
144 | getDescendants( node.left, _children, _hasRecursed );
145 |
146 | } else if( node.type === 'ConditionalExpression' ) {
147 |
148 | getDescendants( node.alternate, _children, _hasRecursed );
149 | getDescendants( node.consequent, _children, _hasRecursed );
150 | }
151 |
152 | return _children;
153 | };
154 |
155 | /**
156 | * Checks whether the node is safe for XSS attacks.
157 | *
158 | * @param {Node} node - Node to check.
159 | *
160 | * @returns {bool} - True, if the node is XSS safe.
161 | */
162 | var isXssSafe = function( node ) {
163 |
164 | // See if the item is commented to be safe.
165 | if( isCommentedSafe( node ) )
166 | return true;
167 |
168 | // Literal nodes and function expressions are okay.
169 | if( node.type === 'Literal' ||
170 | node.type === 'FunctionExpression' ) {
171 | return true;
172 | }
173 |
174 | // Identifiers and member expressions are okay if they resolve to an
175 | // HTML name.
176 | if( node.type === 'Identifier' ||
177 | node.type === 'MemberExpression' ) {
178 |
179 | // isHtmlVariable handles both Identifiers and member expressions.
180 | return isHtmlVariable( node );
181 | }
182 |
183 | // Encode calls are okay.
184 | if( node.type === 'CallExpression' ) {
185 |
186 | return isHtmlOutputFunction( node.callee );
187 | }
188 |
189 | // Assume unsafe.
190 | return false;
191 | };
192 |
193 | /**
194 | * Check for whether the function identifier refers to an encoding function.
195 | *
196 | * @param {Identifier} func - Function identifier to check.
197 | *
198 | * @returns {bool} True, if the function is an encoding function.
199 | */
200 | var isHtmlOutputFunction = function( func ) {
201 |
202 | return allRules.getFunctionRules( func ).htmlOutput ||
203 | re.any( tree.getFullItemName( func ), htmlFunctionRules );
204 | };
205 |
206 |
207 | /**
208 | * Checks whether the function uses raw HTML input.
209 | *
210 | * @param {Identifier} func - Function identifier to check.
211 | *
212 | * @returns {bool} True, if the function is unsafe.
213 | */
214 | var functionAcceptsHtml = function( func ) {
215 | return allRules.getFunctionRules( func ).htmlInput;
216 | };
217 |
218 | /**
219 | * Checks whether the node-tree contains XSS-safe data.
220 | *
221 | * Reports error to ESLint.
222 | *
223 | * @param {Node} node - Root node to check.
224 | * @param {Node} target
225 | * Target node the root is used for. Affects some XSS checks.
226 | */
227 | var checkForXss = function( node, target ) {
228 |
229 | // Skip functions.
230 | // This stops the following from giving errors:
231 | // > htmlEncoder = function() {}
232 | if( node.type === 'FunctionExpression' ||
233 | node.type === 'ObjectExpression' )
234 | return;
235 |
236 | // Get the rules.
237 | var targetRules = allRules.get( target );
238 |
239 | // Get the descendants.
240 | var nodes = getDescendants( node );
241 |
242 | // Check each descendant.
243 | nodes.forEach( function( childNode ) {
244 |
245 | // Return if the parameter is marked as safe in the current context.
246 | if( targetRules.safe === true ) {
247 | return;
248 | } else if( targetRules.safe && targetRules.safe.indexOf &&
249 | targetRules.safe.indexOf( tree.getNodeName( childNode ) ) !== -1 ) {
250 | return;
251 | }
252 |
253 | // Node is okay, if it is safe.
254 | if( isXssSafe( childNode ) )
255 | return;
256 |
257 | // Node wasn't deemed okay. Report error.
258 | var msg = 'Unencoded input \'{{ identifier }}\' used in HTML context';
259 | if( childNode.type === 'CallExpression' ) {
260 | msg = 'Unencoded return value from function \'{{ identifier }}\' ' +
261 | 'used in HTML context';
262 | childNode = childNode.callee;
263 | }
264 |
265 | var identifier = null;
266 | if( childNode.type === 'ObjectExpression' )
267 | identifier = '[Object]';
268 | else if( childNode.type === 'ArrayExpression' )
269 | identifier = '[Array]';
270 | else
271 | identifier = context.getSource( childNode );
272 |
273 | context.report( {
274 | node: childNode,
275 | message: msg,
276 | data: { identifier: identifier }
277 | } );
278 | } );
279 | };
280 |
281 | /**
282 | * Checks whether the node uses HTML.
283 | *
284 | * @param {Node} node - Node to check.
285 | *
286 | * @returns {bool} True, if the node uses HTML.
287 | */
288 | var usesHtml = function( node ) {
289 |
290 | // Check the node type.
291 | if( node.type === 'CallExpression' ) {
292 |
293 | // Check the valid call expression callees.
294 | return functionAcceptsHtml( node.callee );
295 |
296 | } else if( node.type === 'AssignmentExpression' ) {
297 |
298 | // Assignment operator.
299 | // x = y
300 | // HTML-name on the left indicates html expression.
301 | return isHtmlVariable( node.left );
302 |
303 | } else if( node.type === 'VariableDeclarator' ) {
304 |
305 | // Variable declaration.
306 | // var x = y
307 | // HTML-name as the variable name indicates html expression.
308 | return isHtmlVariable( node.id );
309 |
310 | } else if( node.type === 'Property' ) {
311 |
312 | // Property declaration.
313 | // x: y
314 | // HTML-name as the key indicates html property.
315 | return isHtmlVariable( node.key );
316 |
317 | } else if( node.type === 'ArrayExpression' ) {
318 |
319 | // Array expression.
320 | // [ a, b, c ]
321 | return usesHtml( node.parent );
322 |
323 | } else if( node.type === 'ReturnStatement' ) {
324 |
325 | // Return statement.
326 | let func = tree.getParentFunctionIdentifier( node );
327 | if( !func ) return false;
328 |
329 | return isHtmlFunction( func );
330 |
331 | } else if( node.type === 'ArrowFunctionExpression' ) {
332 |
333 | // Return statement.
334 | let func = tree.getParentFunctionIdentifier( node );
335 | if( !func ) return false;
336 |
337 | return isHtmlFunction( func );
338 | }
339 |
340 | return false;
341 | };
342 |
343 | /**
344 | * Checks whether the node meets the criteria of storing HTML content.
345 | *
346 | * Reports error to ESLint.
347 | *
348 | * @param {Node} node - The node to check.
349 | */
350 | var checkHtmlVariable = function( node ) {
351 |
352 | var msg = 'Non-HTML variable \'{{ identifier }}\' is used to store raw HTML';
353 | if( !isXssSafe( node ) ) {
354 | context.report( {
355 | node: node,
356 | message: msg,
357 | data: {
358 | identifier: context.getSource( node )
359 | }
360 | } );
361 | }
362 | };
363 |
364 | /**
365 | * Checks whether the node meets the criteria of storing HTML content.
366 | *
367 | * Reports error to ESLint.
368 | *
369 | * @param {Node} node - The node to check.
370 | * @param {Node} fault
371 | * The node that causes the fail and should be reported as error location.
372 | */
373 | var checkHtmlFunction = function( node, fault ) {
374 |
375 | var msg = 'Non-HTML function \'{{ identifier }}\' returns HTML content';
376 | if( !isXssSafe( node ) ) {
377 | context.report( {
378 | node: fault,
379 | message: msg,
380 | data: {
381 | identifier: context.getSource( node )
382 | }
383 | } );
384 | }
385 | };
386 |
387 | /**
388 | * Checks whether the node meets the criteria of storing HTML content.
389 | *
390 | * Reports error to ESLint.
391 | *
392 | * @param {Node} node - The node to check.
393 | */
394 | var checkFunctionAcceptsHtml = function( node ) {
395 |
396 | if( !functionAcceptsHtml( node ) ) {
397 | context.report( {
398 | node: node,
399 | message: 'HTML passed in to function \'{{ identifier }}\'',
400 | data: {
401 | identifier: context.getSource( node )
402 | }
403 | } );
404 | }
405 | };
406 |
407 | /**
408 | * Checks whether the node name matches the variable naming rule.
409 | *
410 | * @param {Node} node - Node to check
411 | *
412 | * @returns {bool} True, if the node matches HTML variable naming.
413 | */
414 | var isHtmlVariable = function( node ) {
415 |
416 | // Ensure we can get the identifier.
417 | node = tree.getIdentifier( node );
418 | if( !node ) return false;
419 |
420 | // Make the check against the htmlVariableRules regexp.
421 | return re.any( node.name, htmlVariableRules );
422 | };
423 |
424 | /**
425 | * Checks whether the node name matches the function naming rule.
426 | *
427 | * @param {Node} node - Node to check
428 | *
429 | * @returns {bool} True, if the node matches HTML function naming.
430 | */
431 | var isHtmlFunction = function( node ) {
432 |
433 | // Ensure we can get the identifier.
434 | node = tree.getIdentifier( node );
435 | if( !node ) return false;
436 |
437 | // Make the check against the function naming rule.
438 | return re.any( node.name, htmlFunctionRules );
439 | };
440 |
441 | /**
442 | * Checks whether the current node may infect the stack with XSS.
443 | *
444 | * @param {Node} node - Current node.
445 | *
446 | * @returns {bool} True, if the node can infect the stack.
447 | */
448 | var canInfectXss = function( node ) {
449 |
450 | // If we got nothing in the stack, there's nothing to infect.
451 | if( exprStack.length === 0 )
452 | return false;
453 |
454 | // Ensure the node to check is used as part of a 'parameter chain' from
455 | // the top stack node.
456 | //
457 | // This 'parameter chain' is the group of nodes that directly affect the
458 | // node result. It ignores things like function expression argument
459 | // lists and bodies, etc.
460 | //
461 | // We don't want to trigger xss checks in case the identifier
462 | // is the parent object of a function call expression for
463 | // example:
464 | // > html.encode( text )
465 | var top = exprStack[ exprStack.length - 1 ].node;
466 | var parent = node;
467 | do {
468 | var child = parent;
469 | parent = parent.parent;
470 |
471 | if( !tree.isParameter( child, parent ) ) {
472 | return false;
473 | }
474 |
475 | } while( parent !== top );
476 |
477 | // Assume true.
478 | return true;
479 | };
480 |
481 | /**
482 | * Pushes node to the expression stack.
483 | *
484 | * @param {Node} node - Node to push.
485 | */
486 | var pushNode = function( node ) {
487 |
488 | exprStack.push( { node: node } );
489 | };
490 |
491 | /**
492 | * Pops a node from the expression stack and checks it for XSS issues.
493 | */
494 | var exitNode = function() {
495 |
496 | // Quick checks for whether the node is even vulnerable to XSS.
497 | var expr = exprStack.pop();
498 | if( !expr.xss && !usesHtml( expr.node ) )
499 | return;
500 |
501 | // Now we should know there is HTML involved somewhere.
502 |
503 | // Check whether the node has been commented safe.
504 | if( isCommentedSafe( expr.node ) )
505 | return;
506 |
507 | // Check the node based on its type.
508 | if( expr.node.type === 'CallExpression' ) {
509 |
510 | // Call expression.
511 | //
512 | // Ensure the function accepts HTML and none of the arguments have
513 | // XSS issues.
514 | checkFunctionAcceptsHtml( expr.node.callee );
515 | expr.node.arguments.forEach( function( a ) {
516 | checkForXss( a, expr.node );
517 | } );
518 |
519 | } else if( expr.node.type === 'AssignmentExpression' ) {
520 |
521 | // Assignment.
522 | //
523 | // Ensure the target variable is HTML compatible and the assigned
524 | // value doesn't have XSS issues.
525 | checkHtmlVariable( expr.node.left );
526 | checkForXss( expr.node.right, expr.node );
527 |
528 | } else if( expr.node.type === 'VariableDeclarator' ) {
529 |
530 | // New variable initialization.
531 | //
532 | // Ensure the target variable is HTML compatible and the assigned
533 | // value doesn't have XSS issues.
534 | checkHtmlVariable( expr.node.id );
535 | if( expr.node.init )
536 | checkForXss( expr.node.init, expr.node );
537 |
538 | } else if( expr.node.type === 'Property' ) {
539 |
540 | // Property declaration inside an object declaration.
541 | //
542 | // Ensure the target property is HTML compatible and the assigned
543 | // value doesn't have XSS issues.
544 | checkHtmlVariable( expr.node.key );
545 | checkForXss( expr.node.value, expr.node );
546 |
547 | } else if( expr.node.type === 'ReturnStatement' ) {
548 |
549 | // Return statement.
550 | //
551 | // Make sure the function we are returning from is compatible
552 | // with a HTML return value and there are no XSS issues in the
553 | // value returned.
554 |
555 | // Get the closest function scope.
556 | let func = tree.getParentFunctionIdentifier( expr.node );
557 | if( !func ) return;
558 |
559 | checkHtmlFunction( func, expr.node );
560 | checkForXss( expr.node.argument, expr.node );
561 |
562 | } else if( expr.node.type === 'ArrowFunctionExpression' ) {
563 |
564 | // Arrow function expression.
565 | //
566 | // Make sure the function we are returning from is compatible
567 | // with a HTML return value and there are no XSS issues in the
568 | // value returned.
569 |
570 | // Get the closest function scope.
571 | let func = tree.getParentFunctionIdentifier( expr.node );
572 | if( !func ) return;
573 |
574 | checkHtmlFunction( func, func );
575 | checkForXss( expr.node.body, expr.node );
576 | }
577 | };
578 |
579 | var markParentXSS = function() {
580 |
581 | // Ensure the current node is XSS candidate.
582 | var expr = exprStack.pop();
583 | if( !expr.xss && !usesHtml( expr.node ) )
584 | return;
585 |
586 | // Mark the parent element as XSS candidate.
587 | var candidate = getXssCandidateParent( expr.node );
588 | if( candidate )
589 | candidate.xss = true;
590 | };
591 |
592 | /**
593 | * Checks whether the given node is commented to be safe from HTML.
594 | *
595 | * @param {Node} node - The node to check for the comments.
596 | *
597 | * @returns {bool} True, if the node is commented safe.
598 | */
599 | var isCommentedSafe = function( node ) {
600 |
601 | while( node && (
602 | node.type === 'ArrayExpression' ||
603 | node.type === 'Identifier' ||
604 | node.type === 'Literal' ||
605 | node.type === 'CallExpression' ||
606 | node.type === 'BinaryExpression' ||
607 | node.type === 'MemberExpression' ) ) {
608 |
609 | if( nodeHasSafeComment( node ) )
610 | return true;
611 |
612 | node = getCommentParent( node );
613 | }
614 |
615 | return false;
616 | };
617 |
618 | /**
619 | * Gets a parent node that might have a comment that is seemingly
620 | * attached to the current node.
621 | *
622 | * This might differ from normal parent node in cases where the
623 | * physical location of the node isn't at the start of the parent:
624 | *
625 | * /comment/ a + b
626 | *
627 | * Here the comment is attached to the binary expression node 'a+b' instead
628 | * of the a 'a' identifier node.
629 | *
630 | * However 'a' should still be considered commented - but 'b' isn't.
631 | *
632 | * However this function also handles situation such as
633 | * /comment/ ( a + b )
634 | * Where the comment should count for both a and b.
635 | *
636 | * @param {Node} node - The node to get the parent for.
637 | *
638 | * @returns {Node} The practical parent node.
639 | */
640 | var getCommentParent = function( node ) {
641 |
642 | var parent = node.parent;
643 | if( !parent ) return parent;
644 |
645 | // Call expressions don't cause comment inheritance:
646 | // /comment/ foo( unsafe() )
647 | //
648 | // Shouldn't equal:
649 | // foo( /comment/ unsafe() )
650 | if( parent.type === 'CallExpression' )
651 | return null;
652 |
653 | // Binary expressions are a bit confusing when it comes to comment
654 | // parenting. /comment/ x + y belongs to the binary expression instead
655 | // of 'x'.
656 | if( parent.type === 'BinaryExpression' ) {
657 |
658 | // If the node is left side of binary expression, return parent no
659 | // matter what.
660 | if( node === parent.left )
661 | return parent;
662 |
663 | // Get the closest parenthesized binary expression.
664 | while( parent &&
665 | parent.type === 'BinaryExpression' &&
666 | !hasParentheses( parent ) ) {
667 |
668 | parent = parent.parent;
669 | }
670 |
671 | if( parent && parent.type === 'BinaryExpression' )
672 | return parent;
673 |
674 | return null;
675 | }
676 |
677 | return parent;
678 | };
679 |
680 | /**
681 | * Checks whether the node is surrounded by parentheses.
682 | *
683 | * @param {Node} node - Node to check for parentheses.
684 | *
685 | * @returns {bool} True, if the node is surrounded with parentheses.
686 | */
687 | var hasParentheses = function( node ) {
688 | var prevToken = context.getTokenBefore( node );
689 |
690 | return ( prevToken.type === 'Punctuator' && prevToken.value === '(' );
691 | };
692 |
693 | /**
694 | * Checks whether the given node is commented to be safe from HTML.
695 | *
696 | * @param {Node} node - Node to check.
697 | *
698 | * @returns {bool} True, if this specific node has a /safe/ comment.
699 | */
700 | var nodeHasSafeComment = function( node ) {
701 |
702 | // Check all the comments in front of the node for comment 'safe'
703 | var isSafe = false;
704 | var comments = context.getSourceCode().getComments( node );
705 | comments.leading.forEach( function( comment ) {
706 | if( /^\s*safe\s*$/i.exec( comment.value ) )
707 | isSafe = true;
708 | } );
709 |
710 | return isSafe;
711 | };
712 |
713 | /**
714 | * Gets the closest parent node that matches the given type. May return the
715 | * node itself.
716 | *
717 | * @param {Node} node - The node to start the search from.
718 | * @param {string} parentType - The node type to search.
719 | *
720 | * @returns {Node} The closest node of the correct type.
721 | */
722 | var getPathFromParent = function( node, parentType ) {
723 |
724 | var path = [ node ];
725 | while( node && node.type !== parentType ) {
726 | node = node.parent;
727 | path.push( node );
728 | }
729 |
730 | if( !node )
731 | return null;
732 |
733 | path.reverse();
734 | return path;
735 | };
736 |
737 | var getXssCandidateParent = function( node ) {
738 |
739 | // Find the infectable node.
740 | //
741 | // This takes care of call expressions that might use
742 | // passthrough functions. Here we need to check whether the
743 | // current node is in a passthrough position.
744 | for( var ptr = exprStack.length - 1; ptr >= 0; ptr-- ) {
745 |
746 | // Only CallExpressions may pass through the parameters.
747 | var candidate = exprStack[ ptr ];
748 | if( candidate.node.type !== 'CallExpression' )
749 | return candidate;
750 |
751 | // Quick check for whether this is an passthrough at all.
752 | var functionRules = allRules.get( candidate.node );
753 | if( !functionRules.passthrough )
754 | return candidate;
755 |
756 | // The function is at least a partial passthrough.
757 | // Quickly check whether it passes everything through.
758 | if( functionRules.passthrough.obj && functionRules.passthrough.args )
759 | continue;
760 |
761 | // Only obj OR args is passed through. Figure out which one the
762 | // current node is.
763 | var path = getPathFromParent( node, 'CallExpression' );
764 | var callExpr = path[ 0 ];
765 | var callImmediateChild = path[ 1 ];
766 |
767 | var isCallee = callImmediateChild === callExpr.callee;
768 | var isParam = !isCallee;
769 |
770 | // Continue to next stack part if the function passes the obj through
771 | // and the current node is the obj.
772 | if( isCallee && functionRules.passthrough.obj )
773 | continue;
774 |
775 | // Continue to next stack part if the function passes the args through
776 | // and the current node is an argument.
777 | if( isParam && functionRules.passthrough.args )
778 | continue;
779 |
780 | return candidate;
781 | }
782 |
783 | return null;
784 | };
785 |
786 | var infectParentConditional = function( condition, node ) {
787 |
788 | if( exprStack.length > 0 &&
789 | !isCommentedSafe( node ) &&
790 | canInfectXss( node ) &&
791 | condition( node ) ) {
792 |
793 | var infectable = getXssCandidateParent( node );
794 | if( infectable )
795 | infectable.xss = true;
796 | }
797 | };
798 |
799 | // -------------------------------------------------------------------------
800 | // Public
801 | // -------------------------------------------------------------------------
802 |
803 | return {
804 |
805 | 'AssignmentExpression': pushNode,
806 | 'AssignmentExpression:exit': exitNode,
807 | 'VariableDeclarator': pushNode,
808 | 'VariableDeclarator:exit': exitNode,
809 | 'Property': pushNode,
810 | 'Property:exit': exitNode,
811 | 'ReturnStatement': pushNode,
812 | 'ReturnStatement:exit': exitNode,
813 | 'ArrowFunctionExpression': pushNode,
814 | 'ArrowFunctionExpression:exit': exitNode,
815 | 'ArrayExpression': pushNode,
816 | 'ArrayExpression:exit': markParentXSS,
817 |
818 | // Call expressions have a dual nature. They can either infect their
819 | // parents with XSS vulnerabilities or then they can suffer from them.
820 | 'CallExpression': function( node ) {
821 |
822 | // First check whether this expression marks the parent as dirty.
823 | infectParentConditional( function( node ) {
824 | return isHtmlOutputFunction( node.callee );
825 | }, node );
826 | pushNode( node );
827 | },
828 | 'CallExpression:exit': exitNode,
829 |
830 | // Literals infect parents if they contain tags or fragments.
831 | 'Literal': infectParentConditional.bind( null, function( node ) {
832 |
833 | // Skip regex and /*safe*/ strings. Remaining strings infect parent
834 | // if they contain =0.10.0"
27 | },
28 | "license": "ISC"
29 | }
30 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/tests/lib/rules/no-mixed-html.js:
--------------------------------------------------------------------------------
1 | /**
2 | * @fileoverview Checks for missing encoding when concatenating HTML strings
3 | * @author Mikko Rantanen
4 | */
5 |
6 | /* eslint max-len: 0, xss/no-mixed-html: 0 */
7 | 'use strict';
8 |
9 | // -----------------------------------------------------------------------------
10 | // Requirements
11 | // -----------------------------------------------------------------------------
12 |
13 | var rule = require( '../../../lib/rules/no-mixed-html' ),
14 |
15 | RuleTester = require( 'eslint' ).RuleTester;
16 |
17 |
18 | // -----------------------------------------------------------------------------
19 | // Tests
20 | // -----------------------------------------------------------------------------
21 |
22 | var ruleTester = new RuleTester();
23 | ruleTester.run( 'require-encode', rule, {
24 |
25 | valid: [
26 | 'var x = text',
27 | 'x = text',
28 | 'y = x = text',
29 | 'obj.x = text',
30 | 'foo( text )',
31 | 'x.foo( text )',
32 | 'html.foo( text )',
33 | 'f()( text )',
34 | 'match( // )',
35 |
36 | // TODO: Find a way to add test for lone 'return'.
37 | 'function v() { return console.log( optionator.generateHelp() ); }',
38 | {
39 | code: 'test.html( html )',
40 | options: [ { functions: { '.html': { htmlInput: true, htmlOutput: true } } } ]
41 | },
42 | {
43 | code: '$( html )',
44 | options: [ { functions: { '$': { htmlInput: true } } } ]
45 | },
46 | {
47 | code: 'test.html( \'
\' )',
48 | options: [ { functions: { '.html': { htmlInput: true } } } ],
49 | },
50 | {
51 | code: 'window.document.write( \'
\' )',
52 | options: [ { functions: { 'window.document.write': { htmlInput: true } } } ],
53 | },
54 | {
55 | code: 'window.document.write( \'
\' )',
56 | options: [ { functions: { '.document.write': { htmlInput: true } } } ],
57 | },
58 | {
59 | code: 'window.document.write( \'
\' )',
60 | options: [ { functions: { '.write': { htmlInput: true } } } ],
61 | },
62 | 'var x = "a" + "b";',
63 | {
64 | code: 'var html = "" + encode( foo() ) + "
"',
65 | options: [ { functions: { 'encode': { htmlOutput: true } } } ]
66 | },
67 | {
68 | code: 'var html = "" + he.encode( foo() ) + "
"',
69 | options: [ { functions: { 'he.encode': { htmlOutput: true } } } ]
70 | },
71 | 'x = text;',
72 | 'x = "a" + "b";',
73 | {
74 | code: 'html = "" + encode( foo() ) + "
"',
75 | options: [ { functions: { 'encode': { htmlOutput: true } } } ]
76 | },
77 | 'asHtml = varHtml',
78 | 'asHtml = htmlToo = varHtml',
79 | 'htmlEncode = function() {}',
80 | 'decode = function( html ) {}',
81 |
82 | 'htmlMapping = {}',
83 | 'mapping = { html: "" }',
84 | 'values = [ "value" ]',
85 | 'values = [ text ]',
86 | 'text = html ? "a" : "b"',
87 | 'text = html ? foo : bar',
88 | 'html = html ? "
" : "b"',
89 | {
90 | code: 'encoded = "
"',
91 | options: [ { htmlVariableRules: [ 'encoded' ] } ]
92 | },
93 | {
94 | code: 'asEncoded = "
"',
95 | options: [ { htmlVariableRules: [ 'encoded/i' ] } ]
96 | },
97 |
98 | 'htmlItems = [ html ].join()',
99 | {
100 | code: 'htmlItems = assert( html )',
101 | options: [ { functions: { assert: { passthrough: { args: true } } } } ]
102 | },
103 | {
104 | code: 'text = assert( input )',
105 | options: [ { functions: { assert: { passthrough: { args: true } } } } ]
106 | },
107 | {
108 | code: 'text = en_us.format( input )',
109 | options: [ { functions: { '.format': { passthrough: { args: true } } } } ]
110 | },
111 | {
112 | code: 'html = en_us.format( htmlInput )',
113 | options: [ { functions: { '.format': { passthrough: { args: true } } } } ]
114 | },
115 | {
116 | code: 'text = str.format( en_us )',
117 | options: [ { functions: { '.format': { passthrough: { obj: true } } } } ]
118 | },
119 | {
120 | code: 'html = htmlStr.format( en_us )',
121 | options: [ { functions: { '.format': { passthrough: { obj: true } } } } ]
122 | },
123 | 'html = /*safe*/ en_us.format( htmlInputttttt )',
124 |
125 | 'x = /*safe*/ "This is not "',
126 | 'html = "
" + /*safe*/ input + "
"',
127 | 'html = "
" + /*safe*/ obj.value + "
"',
128 | 'text = /*safe*/ stuffAsHtml()',
129 | 'html = /*safe*/ getElement()',
130 | 'html = /*safe*/ getElement()',
131 | 'x = /*safe*/ "This is not " + text',
132 | 'text = /*safe*/ stuffAsHtml() + text',
133 | 'text = /*safe*/ ( "
" + "
" )',
134 | 'text = /*safe*/ html[ 0 ]',
135 | 'html = /*safe*/ window.document[ 0 ]',
136 | {
137 | code: 'if( !$( /*safe*/ document.element ).is() ) {}',
138 | options: [ { functions: { '$': { htmlInput: true, safe: [ 'document', 'this', 'window' ] } } } ]
139 | },
140 | {
141 | code: 'var prompt = $( "
" + "" + /* safe */ text + "" + /* safe */ value + "
" )',
142 | options: [ { functions: { '$': { htmlInput: true } } } ]
143 | },
144 | 'obj = { fooHtml: stuffAsHtml() }',
145 | 'obj = { foo: stuff() }',
146 | {
147 | code: 'html = foo.asHtml()',
148 | options: [ { htmlFunctionRules: [ '\.asHtml$' ] } ]
149 | },
150 | {
151 | code: 'html = fullHtml.left( offset )',
152 | options: [ { functions: { '.left': { passthrough: { obj: true } } } } ]
153 | },
154 | {
155 | code: '$( document )',
156 | options: [ { functions: { '$': { htmlInput: true, safe: [ 'document' ] } } } ],
157 | },
158 | {
159 | code: '$( foobar )',
160 | options: [ { functions: { '$': { htmlInput: true, safe: true } } } ],
161 | },
162 | {
163 | code: '$( ".foo" )',
164 | options: [ { functions: { '$': { htmlInput: true, safe: [ 'document' ] } } } ],
165 | },
166 | {
167 | code: '$( this ).toggle()',
168 | options: [ { functions: {
169 | '$': { htmlInput: true, safe: [ 'document', 'this' ] }
170 | } } ],
171 | },
172 | {
173 | code: '$( "#item-" + CSS.escape( id ) )',
174 | options: [ { functions: {
175 | '$': { htmlInput: true, safe: [ 'document' ] },
176 | 'CSS.escape': { htmlOutput: true }
177 | } } ],
178 | },
179 | 'htmlArrs = [ /*safe*/ [ "
" ], /*safe*/ [ "
" ] ]',
180 | 'htmlArrs = /*safe*/ [ [ "
" ], [ "
" ] ]',
181 |
182 | 'html = function() { return "
" }',
183 | 'text = function() { return input }',
184 | 'text = function( html ) { return input }',
185 | '(function() { return "
" })',
186 | '(function() { return input })',
187 | {
188 | parserOptions: { ecmaVersion: 6 },
189 | code: '(() => "
")',
190 | },
191 | {
192 | parserOptions: { ecmaVersion: 6 },
193 | code: '(() => html)',
194 | },
195 | {
196 | parserOptions: { ecmaVersion: 6 },
197 | code: 'text = (html) => input',
198 | },
199 | {
200 | parserOptions: { ecmaVersion: 6 },
201 | code: '(() => input)',
202 | },
203 | {
204 | parserOptions: { ecmaVersion: 6 },
205 | code: 'html = () => { return "
" }',
206 | },
207 | {
208 | parserOptions: { ecmaVersion: 6 },
209 | code: 'text = () => { return input }',
210 | },
211 | {
212 | parserOptions: { ecmaVersion: 6 },
213 | code: 'text = ( html ) => { return input }',
214 | },
215 | {
216 | parserOptions: { ecmaVersion: 6 },
217 | code: '(() => { return "
" })',
218 | },
219 | {
220 | parserOptions: { ecmaVersion: 6 },
221 | code: '(() => { return input })',
222 | },
223 |
224 | 'html',
225 | 'text',
226 | '"use strict"',
227 | '"
"',
228 | ],
229 |
230 | invalid: [
231 |
232 | {
233 | code: 'var html = "
" + text + "
"',
234 | errors: [ {
235 | message: 'Unencoded input \'text\' used in HTML context',
236 | } ]
237 | },
238 | {
239 | code: 'html = "
" + text + "
"',
240 | errors: [ {
241 | message: 'Unencoded input \'text\' used in HTML context',
242 | } ]
243 | },
244 | {
245 | code: 'x.innerHTML = "
" + text + "
"',
246 | errors: [ {
247 | message: 'Unencoded input \'text\' used in HTML context',
248 | } ]
249 | },
250 | {
251 | code: 'write( html )',
252 | errors: [ {
253 | message: 'HTML passed in to function \'write\'',
254 | } ]
255 | },
256 | {
257 | code: 'f()( html )',
258 | errors: [ {
259 | message: 'HTML passed in to function \'f()\'',
260 | } ]
261 | },
262 | {
263 | code: 'x.html( "
" + text + "
" )',
264 | options: [ { functions: { '.html': { htmlInput: true } } } ],
265 | errors: [ {
266 | message: 'Unencoded input \'text\' used in HTML context',
267 | } ]
268 | },
269 | {
270 | code: 'window.document.write( value )',
271 | options: [ { functions: { 'window.document.write': { htmlInput: true } } } ],
272 | errors: [ {
273 | message: 'Unencoded input \'value\' used in HTML context',
274 | } ]
275 | },
276 | {
277 | code: 'window.document.write( value )',
278 | options: [ { functions: { '.document.write': { htmlInput: true } } } ],
279 | errors: [ {
280 | message: 'Unencoded input \'value\' used in HTML context',
281 | } ]
282 | },
283 | {
284 | code: 'window.document.write( value )',
285 | options: [ { functions: { '.write': { htmlInput: true } } } ],
286 | errors: [ {
287 | message: 'Unencoded input \'value\' used in HTML context',
288 | } ]
289 | },
290 | {
291 | code: 'write( html )',
292 | options: [ { functions: { '.write': { htmlInput: true } } } ],
293 | errors: [ {
294 | message: 'HTML passed in to function \'write\'',
295 | } ]
296 | },
297 | {
298 | code: 'document.write( html )',
299 | options: [ { functions: { '.document.write': { htmlInput: true } } } ],
300 | errors: [ {
301 | message: 'HTML passed in to function \'document.write\'',
302 | } ]
303 | },
304 | {
305 | code: 'var asHtml = text',
306 | errors: [ {
307 | message: 'Unencoded input \'text\' used in HTML context',
308 | } ]
309 | },
310 | {
311 | code: 'asHtml = text',
312 | errors: [ {
313 | message: 'Unencoded input \'text\' used in HTML context',
314 | } ]
315 | },
316 | {
317 | code: 'asHtml = text[ 0 ]',
318 | errors: [ {
319 | message: 'Unencoded input \'text[ 0 ]\' used in HTML context',
320 | } ]
321 | },
322 | {
323 | code: 'x.html( text )',
324 | options: [ { functions: { '.html': { htmlInput: true } } } ],
325 | errors: [ {
326 | message: 'Unencoded input \'text\' used in HTML context',
327 | } ]
328 | },
329 | {
330 | code: 'x.innerHTML = text',
331 | errors: [ {
332 | message: 'Unencoded input \'text\' used in HTML context',
333 | } ]
334 | },
335 |
336 | {
337 | code: 'var x = asHtml',
338 | errors: [ {
339 | message: 'Non-HTML variable \'x\' is used to store raw HTML',
340 | } ]
341 | },
342 | {
343 | code: 'x = asHtml',
344 | errors: [ {
345 | message: 'Non-HTML variable \'x\' is used to store raw HTML',
346 | } ]
347 | },
348 | {
349 | code: 'x( asHtml )',
350 | errors: [ {
351 | message: 'HTML passed in to function \'x\'',
352 | } ]
353 | },
354 | {
355 | code: 'foo.x( asHtml )',
356 | errors: [ {
357 | message: 'HTML passed in to function \'foo.x\'',
358 | } ]
359 | },
360 |
361 | {
362 | code: 'var x = obj.html',
363 | errors: [ {
364 | message: 'Non-HTML variable \'x\' is used to store raw HTML',
365 | } ]
366 | },
367 | {
368 | code: 'x = html[ 0 ]',
369 | errors: [ {
370 | message: 'Non-HTML variable \'x\' is used to store raw HTML',
371 | } ]
372 | },
373 | {
374 | code: 'x = obj.html',
375 | errors: [ {
376 | message: 'Non-HTML variable \'x\' is used to store raw HTML',
377 | } ]
378 | },
379 | {
380 | code: 'x( obj.html )',
381 | errors: [ {
382 | message: 'HTML passed in to function \'x\'',
383 | } ]
384 | },
385 | {
386 | code: 'foo.x( obj.html )',
387 | errors: [ {
388 | message: 'HTML passed in to function \'foo.x\'',
389 | } ]
390 | },
391 |
392 | {
393 | code: 'var x = "
"',
394 | errors: [ {
395 | message: 'Non-HTML variable \'x\' is used to store raw HTML',
396 | } ]
397 | },
398 | {
399 | code: 'x = "
"',
400 | errors: [ {
401 | message: 'Non-HTML variable \'x\' is used to store raw HTML',
402 | } ]
403 | },
404 | {
405 | code: 'x( "
" )',
406 | errors: [ {
407 | message: 'HTML passed in to function \'x\'',
408 | } ]
409 | },
410 | {
411 | code: 'foo.x( "
" )',
412 | errors: [ {
413 | message: 'HTML passed in to function \'foo.x\'',
414 | } ]
415 | },
416 |
417 | {
418 | code: 'foo.stuff( doc.html( text ) )',
419 | options: [ { functions: { '.html': { htmlInput: true } } } ],
420 | errors: [ {
421 | message: 'Unencoded input \'text\' used in HTML context',
422 | } ]
423 | },
424 |
425 | {
426 | code: 'obj = { html: text }',
427 | errors: [ {
428 | message: 'Unencoded input \'text\' used in HTML context',
429 | } ]
430 | },
431 | {
432 | code: 'obj = [ { html: text } ]',
433 | errors: [ {
434 | message: 'Unencoded input \'text\' used in HTML context',
435 | } ]
436 | },
437 | {
438 | code: '_ = { text: "" }',
439 | errors: [ {
440 | message: 'Non-HTML variable \'text\' is used to store raw HTML',
441 | } ]
442 | },
443 | {
444 | code: 'arr = [ html ]',
445 | errors: [ {
446 | message: 'Non-HTML variable \'arr\' is used to store raw HTML',
447 | } ]
448 | },
449 | {
450 | code: 'htmlItems = [ text ]',
451 | errors: [ {
452 | message: 'Unencoded input \'text\' used in HTML context',
453 | } ]
454 | },
455 | {
456 | code: 'htmlItems = [ text ].join()',
457 | errors: [ {
458 | message: 'Unencoded input \'text\' used in HTML context',
459 | } ]
460 | },
461 | {
462 | code: 'textItems = [ html ].join()',
463 | errors: [ {
464 | message: 'Non-HTML variable \'textItems\' is used to store raw HTML',
465 | } ]
466 | },
467 |
468 | {
469 | code: 'text = encode( text )',
470 | options: [ { functions: { 'encode': { htmlOutput: true } } } ],
471 | errors: [ {
472 | message: 'Non-HTML variable \'text\' is used to store raw HTML'
473 | } ]
474 | },
475 | {
476 | code: 'html = html ? foo : "text"',
477 | errors: [ {
478 | message: 'Unencoded input \'foo\' used in HTML context'
479 | } ]
480 | },
481 | {
482 | code: 'html = html ? "text" : foo',
483 | errors: [ {
484 | message: 'Unencoded input \'foo\' used in HTML context'
485 | } ]
486 | },
487 | {
488 | code: 'text = html ? "
" : foo',
489 | errors: [
490 | { message: 'Non-HTML variable \'text\' is used to store raw HTML' },
491 | { message: 'Unencoded input \'foo\' used in HTML context' },
492 | ]
493 | },
494 | {
495 | code: 'text = encode( "foo" )',
496 | options: [ { functions: { 'encode': { htmlOutput: true } } } ],
497 | errors: [
498 | { message: 'Non-HTML variable \'text\' is used to store raw HTML' }
499 | ]
500 | },
501 | {
502 | code: 'var foo = function() { return "
"; }',
503 | options: [ { htmlFunctionRules: [ 'AsHtml$' ] } ],
504 | errors: [
505 | { message: 'Non-HTML function \'foo\' returns HTML content' }
506 | ]
507 | },
508 | {
509 | code: 'var foo = function bar() { return "
"; }',
510 | options: [ { htmlFunctionRules: [ 'AsHtml$' ] } ],
511 | errors: [
512 | { message: 'Non-HTML function \'bar\' returns HTML content' }
513 | ]
514 | },
515 | {
516 | code: 'function bar() { return "
"; }',
517 | options: [ { htmlFunctionRules: [ 'AsHtml$' ] } ],
518 | errors: [
519 | { message: 'Non-HTML function \'bar\' returns HTML content' }
520 | ]
521 | },
522 | {
523 | code: 'function bar() { return barAsHtml(); }',
524 | options: [ { htmlFunctionRules: [ 'AsHtml$' ] } ],
525 | errors: [
526 | { message: 'Non-HTML function \'bar\' returns HTML content' }
527 | ]
528 | },
529 | {
530 | parserOptions: { ecmaVersion: 6 },
531 | code: 'var bar = y => "
"',
532 | options: [ { htmlFunctionRules: [ 'AsHtml$' ] } ],
533 | errors: [
534 | { message: 'Non-HTML function \'bar\' returns HTML content' }
535 | ]
536 | },
537 |
538 | {
539 | code: 'var fooAsHtml = function() { return value; }',
540 | options: [ { htmlFunctionRules: [ 'AsHtml$' ] } ],
541 | errors: [
542 | { message: 'Unencoded input \'value\' used in HTML context' }
543 | ]
544 | },
545 | {
546 | code: 'var foo = function barAsHtml() { return input; }',
547 | options: [ { htmlFunctionRules: [ 'AsHtml$' ] } ],
548 | errors: [
549 | { message: 'Unencoded input \'input\' used in HTML context' }
550 | ]
551 | },
552 | {
553 | code: 'function barAsHtml() { return bar; }',
554 | options: [ { htmlFunctionRules: [ 'AsHtml$' ] } ],
555 | errors: [
556 | { message: 'Unencoded input \'bar\' used in HTML context' }
557 | ]
558 | },
559 | {
560 | parserOptions: { ecmaVersion: 6 },
561 | code: 'var barAsHtml = y => y',
562 | options: [ { htmlFunctionRules: [ 'AsHtml$' ] } ],
563 | errors: [
564 | { message: 'Unencoded input \'y\' used in HTML context' }
565 | ]
566 | },
567 |
568 | {
569 | code: 'var foo = fooAsHtml()',
570 | options: [ { htmlFunctionRules: [ 'AsHtml$' ] } ],
571 | errors: [
572 | { message: 'Non-HTML variable \'foo\' is used to store raw HTML' }
573 | ]
574 | },
575 | {
576 | code: 'var html = foo()',
577 | options: [ { htmlFunctionRules: [ 'AsHtml$' ] } ],
578 | errors: [
579 | { message: 'Unencoded return value from function \'foo\' used in HTML context' }
580 | ]
581 | },
582 |
583 | {
584 | code: 'x = /*foobar*/ "This is not "',
585 | errors: [
586 | { message: 'Non-HTML variable \'x\' is used to store raw HTML' }
587 | ]
588 | },
589 | {
590 | code: 'x = /*safe*/ "This is not " + html',
591 | errors: [
592 | { message: 'Non-HTML variable \'x\' is used to store raw HTML' }
593 | ]
594 | },
595 | {
596 | code: 'obj = { fooHtml: stuff() }',
597 | errors: [
598 | { message: 'Unencoded return value from function \'stuff\' used in HTML context' }
599 | ]
600 | },
601 | {
602 | code: 'obj = { fooHtml: obj.stuff() }',
603 | errors: [
604 | { message: 'Unencoded return value from function \'obj.stuff\' used in HTML context' }
605 | ]
606 | },
607 | {
608 | code: 'obj = { foo: stuffAsHtml() }',
609 | errors: [
610 | { message: 'Non-HTML variable \'foo\' is used to store raw HTML' }
611 | ]
612 | },
613 | {
614 | code: 'arr = [ {}, "
" ]',
615 | errors: [
616 | { message: 'Non-HTML variable \'arr\' is used to store raw HTML' },
617 | { message: 'Unencoded input \'[Object]\' used in HTML context' },
618 | ]
619 | },
620 | {
621 | code: 'arr = [ [], "
" ]',
622 | errors: [
623 | { message: 'Non-HTML variable \'arr\' is used to store raw HTML' },
624 | { message: 'Unencoded input \'[Array]\' used in HTML context' },
625 | ]
626 | },
627 | {
628 | code: 'htmlArr = [ {}, "
" ]',
629 | errors: [
630 | { message: 'Unencoded input \'[Object]\' used in HTML context' },
631 | ]
632 | },
633 | {
634 | code: 'htmlArr = [ [], "
" ]',
635 | errors: [
636 | { message: 'Unencoded input \'[Array]\' used in HTML context' },
637 | ]
638 | },
639 | {
640 | code: 'html = [ html ].join()',
641 | options: [ { functions: { '.join': {} } } ],
642 | errors: [
643 | { message: 'HTML passed in to function \'[ html ].join\'' },
644 | { message: 'Unencoded return value from function \'[ html ].join\' used in HTML context' },
645 | ]
646 | },
647 | {
648 | code: 'text = assert( "
" )',
649 | options: [ { functions: { 'assert': { passthrough: { args: true } } } } ],
650 | errors: [
651 | { message: 'Non-HTML variable \'text\' is used to store raw HTML' },
652 | ]
653 | },
654 | {
655 | code: 'text = assert( "
" )',
656 | errors: [
657 | { message: 'HTML passed in to function \'assert\'' },
658 | ]
659 | },
660 | {
661 | code: 'text = assert( getHtml() )',
662 | options: [ { functions: {
663 | assert: { passthrough: { args: true } },
664 | getHtml: { htmlOutput: true },
665 | } } ],
666 | errors: [
667 | { message: 'Non-HTML variable \'text\' is used to store raw HTML' },
668 | ]
669 | },
670 | {
671 | code: 'html = assert( input )',
672 | options: [ { functions: {
673 | assert: { passthrough: { args: true } },
674 | getHtml: { htmlOutput: true },
675 | } } ],
676 | errors: [ { message: 'Unencoded input \'input\' used in HTML context' }, ]
677 | },
678 | {
679 | code: 'html = en_us.format( input )',
680 | options: [ { functions: { '.format': { passthrough: { obj: true } } } } ],
681 | errors: [ { message: 'Unencoded input \'en_us\' used in HTML context' }, ]
682 | },
683 | {
684 | code: 'html = en_us.format( htmlInput )',
685 | options: [ { functions: { '.format': { passthrough: { obj: true } } } } ],
686 | errors: [
687 | { message: 'HTML passed in to function \'en_us.format\'' },
688 | { message: 'Unencoded input \'en_us\' used in HTML context' },
689 | ]
690 | },
691 | {
692 | code: 'html = foo.format( en_us )',
693 | options: [ { functions: { '.format': { passthrough: { args: true } } } } ],
694 | errors: [ { message: 'Unencoded input \'en_us\' used in HTML context' }, ]
695 | },
696 | {
697 | code: 'html = htmlStr.format( en_us )',
698 | options: [ { functions: { '.format': { passthrough: { args: true } } } } ],
699 | errors: [ { message: 'Unencoded input \'en_us\' used in HTML context' }, ]
700 | },
701 |
702 | {
703 | code: '$( document )',
704 | options: [ { functions: { '$': { htmlInput: true } } } ],
705 | errors: [
706 | { message: 'Unencoded input \'document\' used in HTML context' },
707 | ]
708 | },
709 | {
710 | code: '$( "#item-" + id )',
711 | options: [ { functions: { '$': { htmlInput: true, safe: [ 'document' ] } } } ],
712 | errors: [
713 | { message: 'Unencoded input \'id\' used in HTML context' },
714 | ]
715 | },
716 | {
717 | code: 'htmlArrs = foo ? [ [ "
" ] ] : [ "div" ]',
718 | errors: [
719 | { message: 'Unencoded input \'[Array]\' used in HTML context' },
720 | ]
721 | }
722 | ]
723 | } );
724 |
--------------------------------------------------------------------------------