@lipemat/eslint-config 4.0.5 → 5.0.0-beta.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/helpers/config.js +11 -11
- package/index.js +111 -115
- package/package.json +22 -7
- package/plugins/security/index.js +51 -0
- package/plugins/security/rules/dangerously-set-inner-html.js +62 -0
- package/plugins/security/rules/html-executing-assignment.js +67 -0
- package/plugins/security/rules/html-executing-function.js +129 -0
- package/plugins/security/rules/html-sinks.js +128 -0
- package/plugins/security/rules/html-string-concat.js +58 -0
- package/plugins/security/rules/jquery-executing.js +105 -0
- package/plugins/security/rules/vulnerable-tag-stripping.js +76 -0
- package/plugins/security/rules/window-escaping.js +213 -0
- package/plugins/security/utils/shared.js +47 -0
- package/types/helpers/config.d.ts +22 -0
- package/types/index.d.ts +3 -0
- package/types/plugins/security/index.d.ts +8 -0
- package/types/plugins/security/rules/dangerously-set-inner-html.d.ts +3 -0
- package/types/plugins/security/rules/html-executing-assignment.d.ts +4 -0
- package/types/plugins/security/rules/html-executing-function.d.ts +6 -0
- package/types/plugins/security/rules/html-sinks.d.ts +4 -0
- package/types/plugins/security/rules/html-string-concat.d.ts +3 -0
- package/types/plugins/security/rules/jquery-executing.d.ts +17 -0
- package/types/plugins/security/rules/vulnerable-tag-stripping.d.ts +4 -0
- package/types/plugins/security/rules/window-escaping.d.ts +5 -0
- package/types/plugins/security/utils/shared.d.ts +21 -0
package/helpers/config.js
CHANGED
|
@@ -1,11 +1,9 @@
|
|
|
1
|
-
import {getExtensionsConfig} from '@lipemat/js-boilerplate/helpers/config.js';
|
|
2
|
-
|
|
3
|
-
|
|
1
|
+
import { getExtensionsConfig } from '@lipemat/js-boilerplate/helpers/config.js';
|
|
4
2
|
/**
|
|
5
3
|
* Get a config from our /index.js merged with any
|
|
6
4
|
* matching configuration from the project directory.
|
|
7
5
|
*
|
|
8
|
-
* For instance if we have a file named config/eslint.config.js in our project
|
|
6
|
+
* For instance, if we have a file named config/eslint.config.js in our project
|
|
9
7
|
* we will merge the contents with our config/eslint.config.js in favor of whatever
|
|
10
8
|
* is specified with the project's file.
|
|
11
9
|
*
|
|
@@ -20,12 +18,14 @@ import {getExtensionsConfig} from '@lipemat/js-boilerplate/helpers/config.js';
|
|
|
20
18
|
* config.configs[0].push({extra: 'Extra'});
|
|
21
19
|
* return config
|
|
22
20
|
* }
|
|
23
|
-
* ```
|
|
24
|
-
*
|
|
25
|
-
* @return {Object[]} - `eslint.Linter.Config[]`
|
|
26
21
|
*/
|
|
27
|
-
export function getConfig(
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
22
|
+
export function getConfig(configs) {
|
|
23
|
+
const BASE = {
|
|
24
|
+
configs: configs
|
|
25
|
+
};
|
|
26
|
+
const mergedConfig = {
|
|
27
|
+
...BASE,
|
|
28
|
+
...getExtensionsConfig('eslint.config', BASE)
|
|
29
|
+
};
|
|
30
|
+
return mergedConfig.configs;
|
|
31
31
|
}
|
package/index.js
CHANGED
|
@@ -1,135 +1,131 @@
|
|
|
1
|
-
import {fixupConfigRules} from '@eslint/compat';
|
|
2
|
-
import {FlatCompat} from '@eslint/eslintrc';
|
|
1
|
+
import { fixupConfigRules } from '@eslint/compat';
|
|
2
|
+
import { FlatCompat } from '@eslint/eslintrc';
|
|
3
3
|
import tsPlugin from '@typescript-eslint/eslint-plugin';
|
|
4
4
|
import tsParser from '@typescript-eslint/parser';
|
|
5
|
+
import securityPlugin from './plugins/security/index.js';
|
|
5
6
|
import globals from 'globals';
|
|
6
7
|
import stylisticTs from '@stylistic/eslint-plugin-ts';
|
|
7
|
-
import {getConfig} from './helpers/config.js';
|
|
8
|
-
|
|
9
|
-
|
|
8
|
+
import { getConfig } from './helpers/config.js';
|
|
10
9
|
const flatCompat = new FlatCompat();
|
|
11
|
-
|
|
12
10
|
/**
|
|
13
11
|
* Default config if no extensions override it.
|
|
14
12
|
*
|
|
15
13
|
*/
|
|
16
14
|
const BASE_CONFIG = {
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
15
|
+
languageOptions: {
|
|
16
|
+
ecmaVersion: 7,
|
|
17
|
+
globals: {
|
|
18
|
+
...globals.browser,
|
|
19
|
+
$: 'readonly',
|
|
20
|
+
jQuery: 'readonly',
|
|
21
|
+
},
|
|
22
|
+
parser: tsParser,
|
|
23
|
+
parserOptions: {
|
|
24
|
+
project: './tsconfig.json',
|
|
25
|
+
warnOnUnsupportedTypeScriptVersion: false,
|
|
26
|
+
},
|
|
27
|
+
sourceType: 'module',
|
|
28
|
+
},
|
|
29
|
+
rules: {
|
|
30
|
+
'arrow-parens': [1, 'as-needed'],
|
|
31
|
+
'arrow-spacing': [1, { before: true, after: true }],
|
|
32
|
+
camelcase: [2, { properties: 'never' }],
|
|
33
|
+
indent: [1, 'tab', { SwitchCase: 1 }],
|
|
34
|
+
'lines-around-comment': 'off',
|
|
35
|
+
'jsdoc/require-param': 'off',
|
|
36
|
+
'jsdoc/require-param-type': 'off',
|
|
37
|
+
'jsdoc/require-returns-description': 'off',
|
|
38
|
+
'jsdoc/check-tag-names': [1, { definedTags: ['notice', 'link', 'task', 'ticket', 'note'] }],
|
|
39
|
+
// Parse error with Svelte v4 due to `as` operator.
|
|
40
|
+
'import/named': 'off',
|
|
41
|
+
'import/no-unresolved': 'off',
|
|
42
|
+
'no-console': ['warn', { allow: ['warn', 'error', 'debug'] }],
|
|
43
|
+
'no-constant-binary-expression': ['warn'],
|
|
44
|
+
'no-multiple-empty-lines': ['error', { max: 2 }],
|
|
45
|
+
'object-curly-spacing': [1, 'never'],
|
|
46
|
+
'react/no-unescaped-entities': [2, { forbid: ['>', '}'] }],
|
|
47
|
+
'react/display-name': 'off',
|
|
48
|
+
'react-hooks/rules-of-hooks': 'error',
|
|
49
|
+
'react-hooks/exhaustive-deps': 'warn',
|
|
50
|
+
'react/jsx-curly-spacing': [1, {
|
|
51
|
+
when: 'never',
|
|
52
|
+
allowMultiline: false,
|
|
53
|
+
children: true,
|
|
54
|
+
}],
|
|
55
|
+
'react/prop-types': [2, { skipUndeclared: true }],
|
|
56
|
+
'space-before-blocks': [1, 'always'],
|
|
57
|
+
'space-before-function-paren': ['error', {
|
|
58
|
+
anonymous: 'never',
|
|
59
|
+
named: 'never',
|
|
60
|
+
asyncArrow: 'ignore',
|
|
61
|
+
}],
|
|
62
|
+
'space-in-parens': [2, 'always'],
|
|
63
|
+
'template-curly-spacing': [1, 'never'],
|
|
64
|
+
yoda: [2, 'always', { onlyEquality: true }],
|
|
65
|
+
},
|
|
66
|
+
settings: {
|
|
67
|
+
react: {
|
|
68
|
+
version: '18.0',
|
|
69
|
+
},
|
|
70
|
+
},
|
|
73
71
|
};
|
|
74
|
-
|
|
75
|
-
|
|
76
72
|
const TS_CONFIG = {
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
73
|
+
files: ['**/*.ts', '**/*.tsx'],
|
|
74
|
+
plugins: {
|
|
75
|
+
'@typescript-eslint': tsPlugin,
|
|
76
|
+
'@stylistic/ts': stylisticTs,
|
|
77
|
+
},
|
|
78
|
+
//Rules to override the standard JS ones when we get undesired results for TypeScript may be found here
|
|
79
|
+
//@link https://typescript-eslint.io/rules/
|
|
80
|
+
rules: {
|
|
81
|
+
'jsdoc/no-undefined-types': 'off',
|
|
82
|
+
'no-magic-numbers': 'off',
|
|
83
|
+
'no-redeclare': 'off',
|
|
84
|
+
'no-shadow': 'off',
|
|
85
|
+
'no-undef': 'off',
|
|
86
|
+
'no-unused-vars': 'off',
|
|
87
|
+
semi: 'off',
|
|
88
|
+
'@typescript-eslint/no-empty-object-type': 'warn',
|
|
89
|
+
'@typescript-eslint/no-explicit-any': 'error',
|
|
90
|
+
'@typescript-eslint/no-redeclare': ['error'],
|
|
91
|
+
'@typescript-eslint/no-restricted-types': ['error', {
|
|
92
|
+
types: {
|
|
93
|
+
unknown: 'Use a specific type.',
|
|
94
|
+
},
|
|
95
|
+
}],
|
|
96
|
+
'@typescript-eslint/no-shadow': ['error'],
|
|
97
|
+
'@typescript-eslint/no-unsafe-function-type': 'error',
|
|
98
|
+
'@typescript-eslint/no-unused-vars': 'error',
|
|
99
|
+
'@typescript-eslint/no-wrapper-object-types': 'error',
|
|
100
|
+
'@typescript-eslint/strict-boolean-expressions': ['warn', {
|
|
101
|
+
allowString: false, allowNumber: false,
|
|
102
|
+
}],
|
|
103
|
+
'@stylistic/ts/type-annotation-spacing': ['warn', {
|
|
104
|
+
before: false,
|
|
105
|
+
after: true,
|
|
106
|
+
overrides: {
|
|
107
|
+
arrow: {
|
|
108
|
+
before: true,
|
|
109
|
+
after: true,
|
|
110
|
+
},
|
|
111
|
+
},
|
|
112
|
+
}],
|
|
113
|
+
},
|
|
118
114
|
};
|
|
119
|
-
|
|
120
115
|
/**
|
|
121
116
|
* Merge in any extensions' config.
|
|
122
117
|
*/
|
|
123
|
-
let mergedConfig = [
|
|
118
|
+
let mergedConfig = [BASE_CONFIG, TS_CONFIG];
|
|
124
119
|
try {
|
|
125
|
-
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
|
|
120
|
+
mergedConfig = getConfig(mergedConfig);
|
|
121
|
+
}
|
|
122
|
+
catch (e) {
|
|
123
|
+
console.debug(e);
|
|
124
|
+
// JS Boilerplate is not installed.
|
|
129
125
|
}
|
|
130
|
-
|
|
131
126
|
export default [
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
127
|
+
...securityPlugin.configs.recommended,
|
|
128
|
+
...fixupConfigRules(flatCompat.extends('plugin:@wordpress/eslint-plugin/recommended-with-formatting')),
|
|
129
|
+
...fixupConfigRules(flatCompat.extends('plugin:deprecation/recommended')),
|
|
130
|
+
...mergedConfig,
|
|
135
131
|
];
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lipemat/eslint-config",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "5.0.0-beta.2",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"description": "Eslint configuration for all @lipemat packages",
|
|
6
6
|
"engines": {
|
|
@@ -8,9 +8,12 @@
|
|
|
8
8
|
},
|
|
9
9
|
"type": "module",
|
|
10
10
|
"main": "index.js",
|
|
11
|
+
"types": "types",
|
|
11
12
|
"files": [
|
|
12
13
|
"index.js",
|
|
13
|
-
"helpers
|
|
14
|
+
"helpers/**/*.js",
|
|
15
|
+
"plugins/**/*.js",
|
|
16
|
+
"types"
|
|
14
17
|
],
|
|
15
18
|
"repository": {
|
|
16
19
|
"type": "git",
|
|
@@ -21,16 +24,21 @@
|
|
|
21
24
|
},
|
|
22
25
|
"homepage": "https://github.com/lipemat/eslint-config#readme",
|
|
23
26
|
"scripts": {
|
|
24
|
-
"
|
|
27
|
+
"build": "tsc --project ./plugins --declaration --declarationDir types",
|
|
28
|
+
"prepublishOnly": "yarn run build",
|
|
29
|
+
"test": "lipemat-js-boilerplate test",
|
|
30
|
+
"validate-ts": "tsc --project ./plugins --noEmit",
|
|
31
|
+
"watch": "tsc --project ./plugins --watch --inlineSourceMap"
|
|
25
32
|
},
|
|
26
33
|
"dependencies": {
|
|
27
34
|
"@eslint/compat": "^1.2.4",
|
|
28
35
|
"@eslint/eslintrc": "^3.2.0",
|
|
29
|
-
"@lipemat/js-boilerplate": "^10.
|
|
36
|
+
"@lipemat/js-boilerplate": "^10.14.1",
|
|
30
37
|
"@stylistic/eslint-plugin-ts": "^2.12.1",
|
|
31
38
|
"@types/eslint": "^9",
|
|
32
|
-
"@typescript-eslint/eslint-plugin": "^8.
|
|
33
|
-
"@typescript-eslint/parser": "^8.
|
|
39
|
+
"@typescript-eslint/eslint-plugin": "^8.40.0",
|
|
40
|
+
"@typescript-eslint/parser": "^8.40.0",
|
|
41
|
+
"@typescript-eslint/utils": "^8.40.0",
|
|
34
42
|
"@wordpress/eslint-plugin": "^22.0.0",
|
|
35
43
|
"eslint": "^9",
|
|
36
44
|
"eslint-plugin-deprecation": "^3",
|
|
@@ -41,11 +49,18 @@
|
|
|
41
49
|
"devDependencies": {
|
|
42
50
|
"@lipemat/js-boilerplate-svelte": "^2.0.0",
|
|
43
51
|
"@types/jest": "^29.5.3",
|
|
52
|
+
"@types/jquery": "^3.5.16",
|
|
44
53
|
"@types/node": "^20",
|
|
54
|
+
"@typescript-eslint/rule-tester": "^8.40.0",
|
|
55
|
+
"@typescript-eslint/types": "^8.40.0",
|
|
56
|
+
"dompurify": "^3.2.6",
|
|
45
57
|
"execa": "^5.1.1",
|
|
46
58
|
"jest": "^29",
|
|
47
59
|
"jest-runner-eslint": "^2.2.1",
|
|
48
|
-
"typescript": "
|
|
60
|
+
"typescript": "5.8.3"
|
|
61
|
+
},
|
|
62
|
+
"peerDependencies": {
|
|
63
|
+
"typescript": "^5.8.3"
|
|
49
64
|
},
|
|
50
65
|
"resolutions": {
|
|
51
66
|
"@babel/runtime": "^7.27.0"
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import dangerouslySetInnerHtml from './rules/dangerously-set-inner-html.js';
|
|
2
|
+
import htmlExecutingAssignment from './rules/html-executing-assignment.js';
|
|
3
|
+
import htmlExecutingFunction from './rules/html-executing-function.js';
|
|
4
|
+
import htmlSinks from './rules/html-sinks.js';
|
|
5
|
+
import htmlStringConcat from './rules/html-string-concat.js';
|
|
6
|
+
import jqueryExecuting from './rules/jquery-executing.js';
|
|
7
|
+
import vulnerableTagStripping from './rules/vulnerable-tag-stripping.js';
|
|
8
|
+
import windowEscaping from './rules/window-escaping.js';
|
|
9
|
+
import { readFileSync } from 'fs';
|
|
10
|
+
import { resolve } from 'path';
|
|
11
|
+
const pkg = JSON.parse(readFileSync(resolve('./package.json'), 'utf8'));
|
|
12
|
+
const plugin = {
|
|
13
|
+
meta: {
|
|
14
|
+
name: pkg.name,
|
|
15
|
+
version: pkg.version,
|
|
16
|
+
},
|
|
17
|
+
rules: {
|
|
18
|
+
'dangerously-set-inner-html': dangerouslySetInnerHtml,
|
|
19
|
+
'html-executing-assignment': htmlExecutingAssignment,
|
|
20
|
+
'html-executing-function': htmlExecutingFunction,
|
|
21
|
+
'html-sinks': htmlSinks,
|
|
22
|
+
'html-string-concat': htmlStringConcat,
|
|
23
|
+
'jquery-executing': jqueryExecuting,
|
|
24
|
+
'vulnerable-tag-stripping': vulnerableTagStripping,
|
|
25
|
+
'window-escaping': windowEscaping,
|
|
26
|
+
},
|
|
27
|
+
configs: {
|
|
28
|
+
recommended: [],
|
|
29
|
+
},
|
|
30
|
+
};
|
|
31
|
+
// Freeze the plugin to prevent modifications and use the plugin within.
|
|
32
|
+
plugin.configs = Object.freeze({
|
|
33
|
+
recommended: [
|
|
34
|
+
{
|
|
35
|
+
plugins: {
|
|
36
|
+
'@lipemat/security': plugin,
|
|
37
|
+
},
|
|
38
|
+
rules: {
|
|
39
|
+
'@lipemat/security/dangerously-set-inner-html': 'error',
|
|
40
|
+
'@lipemat/security/html-executing-assignment': 'error',
|
|
41
|
+
'@lipemat/security/html-executing-function': 'error',
|
|
42
|
+
'@lipemat/security/html-sinks': 'error',
|
|
43
|
+
'@lipemat/security/html-string-concat': 'error',
|
|
44
|
+
'@lipemat/security/jquery-executing': 'error',
|
|
45
|
+
'@lipemat/security/vulnerable-tag-stripping': 'error',
|
|
46
|
+
'@lipemat/security/window-escaping': 'error',
|
|
47
|
+
},
|
|
48
|
+
},
|
|
49
|
+
],
|
|
50
|
+
});
|
|
51
|
+
export default plugin;
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { AST_NODE_TYPES } from '@typescript-eslint/utils';
|
|
2
|
+
import { isSanitized } from '../utils/shared.js';
|
|
3
|
+
function isDangerouslySetInnerHTML(node) {
|
|
4
|
+
return ('JSXAttribute' === node.type &&
|
|
5
|
+
'dangerouslySetInnerHTML' === node.name.name);
|
|
6
|
+
}
|
|
7
|
+
function getDangerouslySetInnerHTMLValue(node) {
|
|
8
|
+
// Expecting value like: {{ __html: expr }}
|
|
9
|
+
const val = node.value;
|
|
10
|
+
if (null === val) {
|
|
11
|
+
return null; // No value provided
|
|
12
|
+
}
|
|
13
|
+
if (AST_NODE_TYPES.JSXExpressionContainer !== val.type) {
|
|
14
|
+
return null;
|
|
15
|
+
}
|
|
16
|
+
const expr = val.expression;
|
|
17
|
+
if (AST_NODE_TYPES.ObjectExpression !== expr.type) {
|
|
18
|
+
return null;
|
|
19
|
+
}
|
|
20
|
+
const htmlProp = expr.properties.find(p => (AST_NODE_TYPES.Property === p.type &&
|
|
21
|
+
((AST_NODE_TYPES.Identifier === p.key.type && '__html' === p.key.name) ||
|
|
22
|
+
(AST_NODE_TYPES.Literal === p.key.type && '__html' === p.key.value))));
|
|
23
|
+
if (undefined !== htmlProp && 'value' in htmlProp) {
|
|
24
|
+
return htmlProp.value;
|
|
25
|
+
}
|
|
26
|
+
return null;
|
|
27
|
+
}
|
|
28
|
+
const plugin = {
|
|
29
|
+
defaultOptions: [],
|
|
30
|
+
meta: {
|
|
31
|
+
type: 'problem',
|
|
32
|
+
fixable: 'code',
|
|
33
|
+
docs: {
|
|
34
|
+
description: 'Disallow using unsanitized values in dangerouslySetInnerHTML',
|
|
35
|
+
},
|
|
36
|
+
messages: {
|
|
37
|
+
dangerousInnerHtml: 'Any HTML passed to `dangerouslySetInnerHTML` gets executed. Please make sure it\'s properly escaped.',
|
|
38
|
+
},
|
|
39
|
+
schema: [],
|
|
40
|
+
},
|
|
41
|
+
create(context) {
|
|
42
|
+
return {
|
|
43
|
+
JSXAttribute(node) {
|
|
44
|
+
if (!isDangerouslySetInnerHTML(node)) {
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
const htmlValue = getDangerouslySetInnerHTMLValue(node);
|
|
48
|
+
if (null === htmlValue || isSanitized(htmlValue)) {
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
context.report({
|
|
52
|
+
node,
|
|
53
|
+
messageId: 'dangerousInnerHtml',
|
|
54
|
+
fix: (fixer) => {
|
|
55
|
+
return fixer.replaceText(node, `dangerouslySetInnerHTML={{__html: DOMPurify.sanitize(${context.sourceCode.getText(htmlValue)})}}`);
|
|
56
|
+
},
|
|
57
|
+
});
|
|
58
|
+
},
|
|
59
|
+
};
|
|
60
|
+
},
|
|
61
|
+
};
|
|
62
|
+
export default plugin;
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { AST_NODE_TYPES } from '@typescript-eslint/utils';
|
|
2
|
+
import { isSanitized } from '../utils/shared.js';
|
|
3
|
+
const UNSAFE_PROPERTIES = [
|
|
4
|
+
'innerHTML', 'outerHTML',
|
|
5
|
+
];
|
|
6
|
+
function isUnsafeProperty(propertyName) {
|
|
7
|
+
return UNSAFE_PROPERTIES.includes(propertyName);
|
|
8
|
+
}
|
|
9
|
+
const plugin = {
|
|
10
|
+
defaultOptions: [],
|
|
11
|
+
meta: {
|
|
12
|
+
docs: {
|
|
13
|
+
description: 'Disallow using unsanitized values in HTML executing property assignments',
|
|
14
|
+
},
|
|
15
|
+
fixable: 'code',
|
|
16
|
+
hasSuggestions: true,
|
|
17
|
+
messages: {
|
|
18
|
+
executed: 'Any HTML used with `{{propertyName}}` gets executed. Make sure it\'s properly escaped.',
|
|
19
|
+
// Suggestions
|
|
20
|
+
domPurify: 'Wrap the argument with a `DOMPurify.sanitize()` call.',
|
|
21
|
+
sanitize: 'Wrap the argument with a `sanitize()` call.',
|
|
22
|
+
},
|
|
23
|
+
schema: [],
|
|
24
|
+
type: 'problem',
|
|
25
|
+
},
|
|
26
|
+
create(context) {
|
|
27
|
+
return {
|
|
28
|
+
AssignmentExpression(node) {
|
|
29
|
+
// Handle element.innerHTML = value and element.outerHTML = value
|
|
30
|
+
if (AST_NODE_TYPES.MemberExpression !== node.left.type || !('name' in node.left.property)) {
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
const propertyName = node.left.property.name;
|
|
34
|
+
if (!isUnsafeProperty(propertyName)) {
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
const value = node.right;
|
|
38
|
+
if (!isSanitized(value)) {
|
|
39
|
+
context.report({
|
|
40
|
+
node,
|
|
41
|
+
messageId: 'executed',
|
|
42
|
+
data: {
|
|
43
|
+
propertyName,
|
|
44
|
+
},
|
|
45
|
+
suggest: [
|
|
46
|
+
{
|
|
47
|
+
messageId: 'domPurify',
|
|
48
|
+
fix: (fixer) => {
|
|
49
|
+
const valueText = context.sourceCode.getText(value);
|
|
50
|
+
return fixer.replaceText(value, `DOMPurify.sanitize(${valueText})`);
|
|
51
|
+
},
|
|
52
|
+
},
|
|
53
|
+
{
|
|
54
|
+
messageId: 'sanitize',
|
|
55
|
+
fix: (fixer) => {
|
|
56
|
+
const valueText = context.sourceCode.getText(value);
|
|
57
|
+
return fixer.replaceText(value, `sanitize(${valueText})`);
|
|
58
|
+
},
|
|
59
|
+
},
|
|
60
|
+
],
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
},
|
|
64
|
+
};
|
|
65
|
+
},
|
|
66
|
+
};
|
|
67
|
+
export default plugin;
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import { AST_NODE_TYPES } from '@typescript-eslint/utils';
|
|
2
|
+
import { isDomElementType, isSanitized } from '../utils/shared.js';
|
|
3
|
+
import { isJQueryCall } from './jquery-executing.js';
|
|
4
|
+
const DOCUMENT_METHODS = [
|
|
5
|
+
'document.write',
|
|
6
|
+
'document.writeln',
|
|
7
|
+
];
|
|
8
|
+
const SECOND_ARG_METHODS = new Set([
|
|
9
|
+
'insertAdjacentHTML',
|
|
10
|
+
'setAttribute',
|
|
11
|
+
]);
|
|
12
|
+
const UNSAFE_METHODS = [
|
|
13
|
+
'after', 'append', 'before', 'insertAdjacentHTML', 'prepend', 'replaceWith', 'setAttribute',
|
|
14
|
+
];
|
|
15
|
+
function isDocumentMethod(methodName) {
|
|
16
|
+
return DOCUMENT_METHODS.includes(methodName);
|
|
17
|
+
}
|
|
18
|
+
function isUnsafeMethod(methodName) {
|
|
19
|
+
return UNSAFE_METHODS.includes(methodName);
|
|
20
|
+
}
|
|
21
|
+
function isSecondArgMethod(methodName) {
|
|
22
|
+
return SECOND_ARG_METHODS.has(methodName);
|
|
23
|
+
}
|
|
24
|
+
function getDocumentCall(node) {
|
|
25
|
+
let calleeName = '';
|
|
26
|
+
if (AST_NODE_TYPES.Identifier === node.callee.type) {
|
|
27
|
+
calleeName = node.callee.name;
|
|
28
|
+
}
|
|
29
|
+
else if (AST_NODE_TYPES.MemberExpression === node.callee.type) {
|
|
30
|
+
if ('name' in node.callee.object) {
|
|
31
|
+
calleeName = node.callee.object.name;
|
|
32
|
+
if ('name' in node.callee.property) {
|
|
33
|
+
calleeName += '.' + node.callee.property.name;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
else if ('name' in node.callee.property) {
|
|
37
|
+
calleeName = node.callee.property.name;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
if (isDocumentMethod(calleeName)) {
|
|
41
|
+
return calleeName;
|
|
42
|
+
}
|
|
43
|
+
return null;
|
|
44
|
+
}
|
|
45
|
+
function getElementMethodCall(node) {
|
|
46
|
+
// Detect element.method(userInput) calls
|
|
47
|
+
if (AST_NODE_TYPES.MemberExpression !== node.callee.type || !('name' in node.callee.property)) {
|
|
48
|
+
return null;
|
|
49
|
+
}
|
|
50
|
+
const methodName = node.callee.property.name;
|
|
51
|
+
if (!isUnsafeMethod(methodName)) {
|
|
52
|
+
return null;
|
|
53
|
+
}
|
|
54
|
+
if (isJQueryCall(node)) {
|
|
55
|
+
return null; // Handled in jquery-executing rule
|
|
56
|
+
}
|
|
57
|
+
// This is a generic element method call, not jQuery specific
|
|
58
|
+
return methodName;
|
|
59
|
+
}
|
|
60
|
+
const plugin = {
|
|
61
|
+
defaultOptions: [],
|
|
62
|
+
meta: {
|
|
63
|
+
docs: {
|
|
64
|
+
description: 'Disallow using unsanitized values in functions that execute HTML',
|
|
65
|
+
},
|
|
66
|
+
fixable: 'code',
|
|
67
|
+
hasSuggestions: true,
|
|
68
|
+
messages: {
|
|
69
|
+
'document.write': 'Any HTML used with `document.write` gets executed. Make sure it\'s properly escaped.',
|
|
70
|
+
'document.writeln': 'Any HTML used with `document.writeln` gets executed. Make sure it\'s properly escaped.',
|
|
71
|
+
after: 'Any HTML used with `after` gets executed. Make sure it\'s properly escaped.',
|
|
72
|
+
append: 'Any HTML used with `append` gets executed. Make sure it\'s properly escaped.',
|
|
73
|
+
before: 'Any HTML used with `before` gets executed. Make sure it\'s properly escaped.',
|
|
74
|
+
insertAdjacentHTML: 'Any HTML used with `insertAdjacentHTML` gets executed. Make sure it\'s properly escaped.',
|
|
75
|
+
prepend: 'Any HTML used with `prepend` gets executed. Make sure it\'s properly escaped.',
|
|
76
|
+
replaceWith: 'Any HTML used with `replaceWith` gets executed. Make sure it\'s properly escaped.',
|
|
77
|
+
setAttribute: 'Any HTML used with `setAttribute` can lead to XSS. Make sure it\'s properly escaped.',
|
|
78
|
+
// Suggestions
|
|
79
|
+
domPurify: 'Wrap the argument with a `DOMPurify.sanitize()` call.',
|
|
80
|
+
sanitize: 'Wrap the argument with a `sanitize()` call.',
|
|
81
|
+
},
|
|
82
|
+
schema: [],
|
|
83
|
+
type: 'problem',
|
|
84
|
+
},
|
|
85
|
+
create(context) {
|
|
86
|
+
return {
|
|
87
|
+
CallExpression(node) {
|
|
88
|
+
let method;
|
|
89
|
+
const documentMethod = getDocumentCall(node);
|
|
90
|
+
if (null !== documentMethod) {
|
|
91
|
+
method = documentMethod;
|
|
92
|
+
}
|
|
93
|
+
else {
|
|
94
|
+
method = getElementMethodCall(node);
|
|
95
|
+
if (null === method) {
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
let arg = node.arguments[0];
|
|
100
|
+
if (isSecondArgMethod(method)) {
|
|
101
|
+
arg = node.arguments[1];
|
|
102
|
+
}
|
|
103
|
+
if (!isSanitized(arg) && !isDomElementType(arg, context)) {
|
|
104
|
+
context.report({
|
|
105
|
+
node,
|
|
106
|
+
messageId: method,
|
|
107
|
+
suggest: [
|
|
108
|
+
{
|
|
109
|
+
messageId: 'domPurify',
|
|
110
|
+
fix: (fixer) => {
|
|
111
|
+
const argText = context.sourceCode.getText(arg);
|
|
112
|
+
return fixer.replaceText(arg, `DOMPurify.sanitize(${argText})`);
|
|
113
|
+
},
|
|
114
|
+
},
|
|
115
|
+
{
|
|
116
|
+
messageId: 'sanitize',
|
|
117
|
+
fix: (fixer) => {
|
|
118
|
+
const argText = context.sourceCode.getText(arg);
|
|
119
|
+
return fixer.replaceText(arg, `sanitize(${argText})`);
|
|
120
|
+
},
|
|
121
|
+
},
|
|
122
|
+
],
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
},
|
|
126
|
+
};
|
|
127
|
+
},
|
|
128
|
+
};
|
|
129
|
+
export default plugin;
|