@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 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( mergedConfig ) {
28
- const BASE = {configs: mergedConfig};
29
- mergedConfig = {...BASE, ...getExtensionsConfig( 'eslint.config', BASE )};
30
- return mergedConfig.configs;
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
- languageOptions: {
18
- ecmaVersion: 7,
19
- globals: {
20
- ...globals.browser,
21
- $: 'readonly',
22
- jQuery: 'readonly',
23
- },
24
- parser: tsParser,
25
- parserOptions: {
26
- project: './tsconfig.json',
27
- warnOnUnsupportedTypeScriptVersion: false,
28
- },
29
- sourceType: 'module',
30
- },
31
- rules: {
32
- 'arrow-parens': [ 1, 'as-needed' ],
33
- 'arrow-spacing': [ 1, {before: true, after: true} ],
34
- camelcase: [ 2, {properties: 'never'} ],
35
- indent: [ 1, 'tab', {SwitchCase: 1} ],
36
- 'lines-around-comment': 'off',
37
- 'jsdoc/require-param': 'off',
38
- 'jsdoc/require-param-type': 'off',
39
- 'jsdoc/require-returns-description': 'off',
40
- 'jsdoc/check-tag-names': [ 1, {definedTags: [ 'notice', 'link', 'task', 'ticket', 'note' ]} ],
41
- // Parse error with Svelte v4 due to `as` operator.
42
- 'import/named': 'off',
43
- 'import/no-unresolved': 'off',
44
- 'no-console': [ 'warn', {allow: [ 'warn', 'error', 'debug' ]} ],
45
- 'no-constant-binary-expression': [ 'warn' ],
46
- 'no-multiple-empty-lines': [ 'error', {max: 2} ],
47
- 'object-curly-spacing': [ 1, 'never' ],
48
- 'react/no-unescaped-entities': [ 2, {forbid: [ '>', '}' ]} ],
49
- 'react/display-name': 'off',
50
- 'react-hooks/rules-of-hooks': 'error',
51
- 'react-hooks/exhaustive-deps': 'warn',
52
- 'react/jsx-curly-spacing': [ 1, {
53
- when: 'never',
54
- allowMultiline: false,
55
- children: true,
56
- } ],
57
- 'react/prop-types': [ 2, {skipUndeclared: true} ],
58
- 'space-before-blocks': [ 1, 'always' ],
59
- 'space-before-function-paren': [ 'error', {
60
- anonymous: 'never',
61
- named: 'never',
62
- asyncArrow: 'ignore',
63
- } ],
64
- 'space-in-parens': [ 2, 'always' ],
65
- 'template-curly-spacing': [ 1, 'never' ],
66
- yoda: [ 2, 'always', {onlyEquality: true} ],
67
- },
68
- settings: {
69
- react: {
70
- version: '18.0',
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
- files: [ '**/*.ts', '**/*.tsx' ],
78
- plugins: {
79
- '@typescript-eslint': tsPlugin,
80
- '@stylistic/ts': stylisticTs,
81
- },
82
- //Rules to override the standard JS ones when we get undesired results for TypeScript may be found here
83
- //@link https://typescript-eslint.io/rules/
84
- rules: {
85
- 'jsdoc/no-undefined-types': 'off',
86
- 'no-magic-numbers': 'off',
87
- 'no-redeclare': 'off',
88
- 'no-shadow': 'off',
89
- 'no-undef': 'off',
90
- 'no-unused-vars': 'off',
91
- semi: 'off',
92
- '@typescript-eslint/no-empty-object-type': 'warn',
93
- '@typescript-eslint/no-explicit-any': 'error',
94
- '@typescript-eslint/no-redeclare': [ 'error' ],
95
- '@typescript-eslint/no-restricted-types': [ 'error', {
96
- types: {
97
- unknown: 'Use a specific type.',
98
- },
99
- } ],
100
- '@typescript-eslint/no-shadow': [ 'error' ],
101
- '@typescript-eslint/no-unsafe-function-type': 'error',
102
- '@typescript-eslint/no-unused-vars': 'error',
103
- '@typescript-eslint/no-wrapper-object-types': 'error',
104
- '@typescript-eslint/strict-boolean-expressions': [ 'warn', {
105
- allowString: false, allowNumber: false,
106
- } ],
107
- '@stylistic/ts/type-annotation-spacing': [ 'warn', {
108
- before: false,
109
- after: true,
110
- overrides: {
111
- arrow: {
112
- before: true,
113
- after: true,
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 = [ BASE_CONFIG, TS_CONFIG ];
118
+ let mergedConfig = [BASE_CONFIG, TS_CONFIG];
124
119
  try {
125
- mergedConfig = getConfig( mergedConfig );
126
- } catch ( e ) {
127
- console.debug( e );
128
- // JS Boilerplate is not installed.
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
- ...fixupConfigRules( flatCompat.extends( 'plugin:@wordpress/eslint-plugin/recommended-with-formatting' ) ),
133
- ...fixupConfigRules( flatCompat.extends( 'plugin:deprecation/recommended' ) ),
134
- ...mergedConfig,
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": "4.0.5",
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
- "test": "lipemat-js-boilerplate test"
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.13.1",
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.18.0",
33
- "@typescript-eslint/parser": "^8.18.0",
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": "^5.7.2"
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;