@lipemat/eslint-config 5.0.0-beta.2 → 5.0.0-beta.4
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/index.js +8 -8
- package/package.json +1 -1
- package/plugins/security/index.js +15 -17
- package/plugins/security/rules/dangerously-set-inner-html.js +18 -4
- package/plugins/security/rules/html-executing-assignment.js +7 -8
- package/plugins/security/rules/html-executing-function.js +10 -10
- package/plugins/security/rules/html-sinks.js +5 -11
- package/plugins/security/rules/html-string-concat.js +6 -1
- package/plugins/security/rules/jquery-executing.js +2 -3
- package/plugins/security/rules/vulnerable-tag-stripping.js +0 -1
- package/plugins/security/rules/window-escaping.js +13 -46
- package/plugins/security/utils/shared.js +36 -5
- package/types/plugins/security/index.d.ts +1 -1
- package/types/plugins/security/rules/dangerously-set-inner-html.d.ts +2 -1
- package/types/plugins/security/rules/html-string-concat.d.ts +7 -1
- package/types/plugins/security/utils/shared.d.ts +20 -2
package/index.js
CHANGED
|
@@ -7,10 +7,6 @@ import globals from 'globals';
|
|
|
7
7
|
import stylisticTs from '@stylistic/eslint-plugin-ts';
|
|
8
8
|
import { getConfig } from './helpers/config.js';
|
|
9
9
|
const flatCompat = new FlatCompat();
|
|
10
|
-
/**
|
|
11
|
-
* Default config if no extensions override it.
|
|
12
|
-
*
|
|
13
|
-
*/
|
|
14
10
|
const BASE_CONFIG = {
|
|
15
11
|
languageOptions: {
|
|
16
12
|
ecmaVersion: 7,
|
|
@@ -115,16 +111,20 @@ const TS_CONFIG = {
|
|
|
115
111
|
/**
|
|
116
112
|
* Merge in any extensions' config.
|
|
117
113
|
*/
|
|
118
|
-
|
|
114
|
+
const defaultConfig = [
|
|
115
|
+
BASE_CONFIG,
|
|
116
|
+
TS_CONFIG,
|
|
117
|
+
securityPlugin.configs.recommended,
|
|
118
|
+
];
|
|
119
|
+
let mergedConfig = [];
|
|
119
120
|
try {
|
|
120
|
-
mergedConfig = getConfig(
|
|
121
|
+
mergedConfig = getConfig(defaultConfig);
|
|
121
122
|
}
|
|
122
123
|
catch (e) {
|
|
124
|
+
// JS Boilerplate is likely not installed.
|
|
123
125
|
console.debug(e);
|
|
124
|
-
// JS Boilerplate is not installed.
|
|
125
126
|
}
|
|
126
127
|
export default [
|
|
127
|
-
...securityPlugin.configs.recommended,
|
|
128
128
|
...fixupConfigRules(flatCompat.extends('plugin:@wordpress/eslint-plugin/recommended-with-formatting')),
|
|
129
129
|
...fixupConfigRules(flatCompat.extends('plugin:deprecation/recommended')),
|
|
130
130
|
...mergedConfig,
|
package/package.json
CHANGED
|
@@ -25,27 +25,25 @@ const plugin = {
|
|
|
25
25
|
'window-escaping': windowEscaping,
|
|
26
26
|
},
|
|
27
27
|
configs: {
|
|
28
|
-
recommended:
|
|
28
|
+
recommended: {},
|
|
29
29
|
},
|
|
30
30
|
};
|
|
31
31
|
// Freeze the plugin to prevent modifications and use the plugin within.
|
|
32
32
|
plugin.configs = Object.freeze({
|
|
33
|
-
recommended:
|
|
34
|
-
{
|
|
35
|
-
|
|
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
|
-
},
|
|
33
|
+
recommended: {
|
|
34
|
+
plugins: {
|
|
35
|
+
'@lipemat/security': plugin,
|
|
48
36
|
},
|
|
49
|
-
|
|
37
|
+
rules: {
|
|
38
|
+
'@lipemat/security/dangerously-set-inner-html': 'error',
|
|
39
|
+
'@lipemat/security/html-executing-assignment': 'error',
|
|
40
|
+
'@lipemat/security/html-executing-function': 'error',
|
|
41
|
+
'@lipemat/security/html-sinks': 'error',
|
|
42
|
+
'@lipemat/security/html-string-concat': 'error',
|
|
43
|
+
'@lipemat/security/jquery-executing': 'error',
|
|
44
|
+
'@lipemat/security/vulnerable-tag-stripping': 'error',
|
|
45
|
+
'@lipemat/security/window-escaping': 'error',
|
|
46
|
+
},
|
|
47
|
+
},
|
|
50
48
|
});
|
|
51
49
|
export default plugin;
|
|
@@ -29,12 +29,15 @@ const plugin = {
|
|
|
29
29
|
defaultOptions: [],
|
|
30
30
|
meta: {
|
|
31
31
|
type: 'problem',
|
|
32
|
-
|
|
32
|
+
hasSuggestions: true,
|
|
33
33
|
docs: {
|
|
34
34
|
description: 'Disallow using unsanitized values in dangerouslySetInnerHTML',
|
|
35
35
|
},
|
|
36
36
|
messages: {
|
|
37
37
|
dangerousInnerHtml: 'Any HTML passed to `dangerouslySetInnerHTML` gets executed. Please make sure it\'s properly escaped.',
|
|
38
|
+
// Suggestions
|
|
39
|
+
domPurify: 'Wrap the content with a `DOMPurify.sanitize()` call.',
|
|
40
|
+
sanitize: 'Wrap the content with a `sanitize()` call.',
|
|
38
41
|
},
|
|
39
42
|
schema: [],
|
|
40
43
|
},
|
|
@@ -51,9 +54,20 @@ const plugin = {
|
|
|
51
54
|
context.report({
|
|
52
55
|
node,
|
|
53
56
|
messageId: 'dangerousInnerHtml',
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
+
suggest: [
|
|
58
|
+
{
|
|
59
|
+
messageId: 'domPurify',
|
|
60
|
+
fix: (fixer) => {
|
|
61
|
+
return fixer.replaceText(node, `dangerouslySetInnerHTML={{__html: DOMPurify.sanitize( ${context.sourceCode.getText(htmlValue)} )}}`);
|
|
62
|
+
},
|
|
63
|
+
},
|
|
64
|
+
{
|
|
65
|
+
messageId: 'sanitize',
|
|
66
|
+
fix: (fixer) => {
|
|
67
|
+
return fixer.replaceText(node, `dangerouslySetInnerHTML={{__html: sanitize( ${context.sourceCode.getText(htmlValue)} )}}`);
|
|
68
|
+
},
|
|
69
|
+
},
|
|
70
|
+
],
|
|
57
71
|
});
|
|
58
72
|
},
|
|
59
73
|
};
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { AST_NODE_TYPES } from '@typescript-eslint/utils';
|
|
2
|
-
import { isSanitized } from '../utils/shared.js';
|
|
2
|
+
import { isDomElementType, isSafeLiteralString, isSanitized } from '../utils/shared.js';
|
|
3
3
|
const UNSAFE_PROPERTIES = [
|
|
4
|
-
'innerHTML',
|
|
4
|
+
'innerHTML',
|
|
5
|
+
'outerHTML',
|
|
5
6
|
];
|
|
6
7
|
function isUnsafeProperty(propertyName) {
|
|
7
8
|
return UNSAFE_PROPERTIES.includes(propertyName);
|
|
@@ -12,7 +13,6 @@ const plugin = {
|
|
|
12
13
|
docs: {
|
|
13
14
|
description: 'Disallow using unsanitized values in HTML executing property assignments',
|
|
14
15
|
},
|
|
15
|
-
fixable: 'code',
|
|
16
16
|
hasSuggestions: true,
|
|
17
17
|
messages: {
|
|
18
18
|
executed: 'Any HTML used with `{{propertyName}}` gets executed. Make sure it\'s properly escaped.',
|
|
@@ -26,8 +26,7 @@ const plugin = {
|
|
|
26
26
|
create(context) {
|
|
27
27
|
return {
|
|
28
28
|
AssignmentExpression(node) {
|
|
29
|
-
|
|
30
|
-
if (AST_NODE_TYPES.MemberExpression !== node.left.type || !('name' in node.left.property)) {
|
|
29
|
+
if (AST_NODE_TYPES.MemberExpression !== node.left.type || AST_NODE_TYPES.Identifier !== node.left.property.type) {
|
|
31
30
|
return;
|
|
32
31
|
}
|
|
33
32
|
const propertyName = node.left.property.name;
|
|
@@ -35,7 +34,7 @@ const plugin = {
|
|
|
35
34
|
return;
|
|
36
35
|
}
|
|
37
36
|
const value = node.right;
|
|
38
|
-
if (!isSanitized(value)) {
|
|
37
|
+
if (!isSafeLiteralString(value) && !isSanitized(value) && isDomElementType(node.left.object, context)) {
|
|
39
38
|
context.report({
|
|
40
39
|
node,
|
|
41
40
|
messageId: 'executed',
|
|
@@ -47,14 +46,14 @@ const plugin = {
|
|
|
47
46
|
messageId: 'domPurify',
|
|
48
47
|
fix: (fixer) => {
|
|
49
48
|
const valueText = context.sourceCode.getText(value);
|
|
50
|
-
return fixer.replaceText(value, `DOMPurify.sanitize(${valueText})`);
|
|
49
|
+
return fixer.replaceText(value, `DOMPurify.sanitize( ${valueText} )`);
|
|
51
50
|
},
|
|
52
51
|
},
|
|
53
52
|
{
|
|
54
53
|
messageId: 'sanitize',
|
|
55
54
|
fix: (fixer) => {
|
|
56
55
|
const valueText = context.sourceCode.getText(value);
|
|
57
|
-
return fixer.replaceText(value, `sanitize(${valueText})`);
|
|
56
|
+
return fixer.replaceText(value, `sanitize( ${valueText} )`);
|
|
58
57
|
},
|
|
59
58
|
},
|
|
60
59
|
],
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { AST_NODE_TYPES } from '@typescript-eslint/utils';
|
|
2
|
-
import { isDomElementType, isSanitized } from '../utils/shared.js';
|
|
2
|
+
import { isDomElementType, isSafeLiteralString, isSanitized } from '../utils/shared.js';
|
|
3
3
|
import { isJQueryCall } from './jquery-executing.js';
|
|
4
4
|
const DOCUMENT_METHODS = [
|
|
5
5
|
'document.write',
|
|
@@ -42,19 +42,20 @@ function getDocumentCall(node) {
|
|
|
42
42
|
}
|
|
43
43
|
return null;
|
|
44
44
|
}
|
|
45
|
-
function getElementMethodCall(node) {
|
|
46
|
-
|
|
47
|
-
if (AST_NODE_TYPES.MemberExpression !== node.callee.type || !('name' in node.callee.property)) {
|
|
45
|
+
function getElementMethodCall(node, context) {
|
|
46
|
+
if (AST_NODE_TYPES.MemberExpression !== node.callee.type || AST_NODE_TYPES.Identifier !== node.callee.property.type) {
|
|
48
47
|
return null;
|
|
49
48
|
}
|
|
50
49
|
const methodName = node.callee.property.name;
|
|
50
|
+
if (!isDomElementType(node.callee.object, context)) {
|
|
51
|
+
return null; // We only care about DOM element method calls.
|
|
52
|
+
}
|
|
51
53
|
if (!isUnsafeMethod(methodName)) {
|
|
52
54
|
return null;
|
|
53
55
|
}
|
|
54
56
|
if (isJQueryCall(node)) {
|
|
55
57
|
return null; // Handled in jquery-executing rule
|
|
56
58
|
}
|
|
57
|
-
// This is a generic element method call, not jQuery specific
|
|
58
59
|
return methodName;
|
|
59
60
|
}
|
|
60
61
|
const plugin = {
|
|
@@ -63,7 +64,6 @@ const plugin = {
|
|
|
63
64
|
docs: {
|
|
64
65
|
description: 'Disallow using unsanitized values in functions that execute HTML',
|
|
65
66
|
},
|
|
66
|
-
fixable: 'code',
|
|
67
67
|
hasSuggestions: true,
|
|
68
68
|
messages: {
|
|
69
69
|
'document.write': 'Any HTML used with `document.write` gets executed. Make sure it\'s properly escaped.',
|
|
@@ -91,7 +91,7 @@ const plugin = {
|
|
|
91
91
|
method = documentMethod;
|
|
92
92
|
}
|
|
93
93
|
else {
|
|
94
|
-
method = getElementMethodCall(node);
|
|
94
|
+
method = getElementMethodCall(node, context);
|
|
95
95
|
if (null === method) {
|
|
96
96
|
return;
|
|
97
97
|
}
|
|
@@ -100,7 +100,7 @@ const plugin = {
|
|
|
100
100
|
if (isSecondArgMethod(method)) {
|
|
101
101
|
arg = node.arguments[1];
|
|
102
102
|
}
|
|
103
|
-
if (!isSanitized(arg) && !isDomElementType(arg, context)) {
|
|
103
|
+
if (!isSafeLiteralString(arg) && !isSanitized(arg) && !isDomElementType(arg, context)) {
|
|
104
104
|
context.report({
|
|
105
105
|
node,
|
|
106
106
|
messageId: method,
|
|
@@ -109,14 +109,14 @@ const plugin = {
|
|
|
109
109
|
messageId: 'domPurify',
|
|
110
110
|
fix: (fixer) => {
|
|
111
111
|
const argText = context.sourceCode.getText(arg);
|
|
112
|
-
return fixer.replaceText(arg, `DOMPurify.sanitize(${argText})`);
|
|
112
|
+
return fixer.replaceText(arg, `DOMPurify.sanitize( ${argText} )`);
|
|
113
113
|
},
|
|
114
114
|
},
|
|
115
115
|
{
|
|
116
116
|
messageId: 'sanitize',
|
|
117
117
|
fix: (fixer) => {
|
|
118
118
|
const argText = context.sourceCode.getText(arg);
|
|
119
|
-
return fixer.replaceText(arg, `sanitize(${argText})`);
|
|
119
|
+
return fixer.replaceText(arg, `sanitize( ${argText} )`);
|
|
120
120
|
},
|
|
121
121
|
},
|
|
122
122
|
],
|
|
@@ -1,11 +1,5 @@
|
|
|
1
1
|
import { AST_NODE_TYPES } from '@typescript-eslint/utils';
|
|
2
|
-
import { isSanitized, isStringLike } from '../utils/shared.js';
|
|
3
|
-
/**
|
|
4
|
-
* Check if a node is a literal string
|
|
5
|
-
*/
|
|
6
|
-
function isLiteralString(node) {
|
|
7
|
-
return AST_NODE_TYPES.Literal === node.type && 'string' === typeof node.value;
|
|
8
|
-
}
|
|
2
|
+
import { isLiteralString, isSanitized, isStringLike } from '../utils/shared.js';
|
|
9
3
|
/**
|
|
10
4
|
* Get the callee name from a call expression
|
|
11
5
|
*/
|
|
@@ -87,11 +81,11 @@ const plugin = {
|
|
|
87
81
|
suggest: [
|
|
88
82
|
{
|
|
89
83
|
messageId: 'domPurify',
|
|
90
|
-
fix: fixer => fixer.replaceText(firstArg, `DOMPurify.sanitize(${argText})`),
|
|
84
|
+
fix: fixer => fixer.replaceText(firstArg, `DOMPurify.sanitize( ${argText} )`),
|
|
91
85
|
},
|
|
92
86
|
{
|
|
93
87
|
messageId: 'sanitize',
|
|
94
|
-
fix: fixer => fixer.replaceText(firstArg, `sanitize(${argText})`),
|
|
88
|
+
fix: fixer => fixer.replaceText(firstArg, `sanitize( ${argText} )`),
|
|
95
89
|
},
|
|
96
90
|
],
|
|
97
91
|
});
|
|
@@ -111,11 +105,11 @@ const plugin = {
|
|
|
111
105
|
suggest: [
|
|
112
106
|
{
|
|
113
107
|
messageId: 'domPurify',
|
|
114
|
-
fix: fixer => fixer.replaceText(node.right, `DOMPurify.sanitize(${rightText})`),
|
|
108
|
+
fix: fixer => fixer.replaceText(node.right, `DOMPurify.sanitize( ${rightText} )`),
|
|
115
109
|
},
|
|
116
110
|
{
|
|
117
111
|
messageId: 'sanitize',
|
|
118
|
-
fix: fixer => fixer.replaceText(node.right, `sanitize(${rightText})`),
|
|
112
|
+
fix: fixer => fixer.replaceText(node.right, `sanitize( ${rightText} )`),
|
|
119
113
|
},
|
|
120
114
|
],
|
|
121
115
|
});
|
|
@@ -3,7 +3,12 @@ function isStringConcat(node) {
|
|
|
3
3
|
// 'foo' + userInput + 'bar' (HTML-like only)
|
|
4
4
|
return AST_NODE_TYPES.BinaryExpression === node.type && '+' === node.operator && hasHtmlLikeStrings(node);
|
|
5
5
|
}
|
|
6
|
-
|
|
6
|
+
/**
|
|
7
|
+
* Check if an expression contains any HTML-like strings.
|
|
8
|
+
* - Looks for `<` or `>` characters in string literals and template literals.
|
|
9
|
+
* - Recursively checks binary expressions with the ` + ` operator.
|
|
10
|
+
*/
|
|
11
|
+
export function hasHtmlLikeStrings(node) {
|
|
7
12
|
if (AST_NODE_TYPES.Literal === node.type && 'string' === typeof node.value) {
|
|
8
13
|
return /[<>]/.test(node.value);
|
|
9
14
|
}
|
|
@@ -55,7 +55,6 @@ const plugin = {
|
|
|
55
55
|
docs: {
|
|
56
56
|
description: 'Disallow using unsanitized values in jQuery methods that execute HTML',
|
|
57
57
|
},
|
|
58
|
-
fixable: 'code',
|
|
59
58
|
hasSuggestions: true,
|
|
60
59
|
messages: {
|
|
61
60
|
needsEscaping: 'Any HTML used with `{{methodName}}` gets executed. Make sure it\'s properly escaped.',
|
|
@@ -84,14 +83,14 @@ const plugin = {
|
|
|
84
83
|
messageId: 'domPurify',
|
|
85
84
|
fix: (fixer) => {
|
|
86
85
|
const argText = context.sourceCode.getText(arg);
|
|
87
|
-
return fixer.replaceText(arg, `DOMPurify.sanitize(${argText})`);
|
|
86
|
+
return fixer.replaceText(arg, `DOMPurify.sanitize( ${argText} )`);
|
|
88
87
|
},
|
|
89
88
|
},
|
|
90
89
|
{
|
|
91
90
|
messageId: 'sanitize',
|
|
92
91
|
fix: (fixer) => {
|
|
93
92
|
const argText = context.sourceCode.getText(arg);
|
|
94
|
-
return fixer.replaceText(arg, `sanitize(${argText})`);
|
|
93
|
+
return fixer.replaceText(arg, `sanitize( ${argText} )`);
|
|
95
94
|
},
|
|
96
95
|
},
|
|
97
96
|
],
|
|
@@ -33,7 +33,6 @@ const plugin = {
|
|
|
33
33
|
docs: {
|
|
34
34
|
description: 'Disallow jQuery .html().text() chaining which can lead to XSS through tag stripping',
|
|
35
35
|
},
|
|
36
|
-
fixable: 'code',
|
|
37
36
|
hasSuggestions: true,
|
|
38
37
|
messages: {
|
|
39
38
|
vulnerableTagStripping: 'Using .html().text() can lead to XSS vulnerabilities through tag stripping. Use only .text()',
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { AST_NODE_TYPES } from '@typescript-eslint/utils';
|
|
2
|
-
import { isSanitized } from '../utils/shared.js';
|
|
2
|
+
import { isLiteralString, isSanitized } from '../utils/shared.js';
|
|
3
3
|
// Window and location properties that need special handling
|
|
4
4
|
const LOCATION_PROPS = new Set(['href', 'src', 'action',
|
|
5
5
|
'protocol', 'host', 'hostname', 'pathname', 'search', 'hash', 'username', 'port', 'name', 'status',
|
|
@@ -9,37 +9,7 @@ export function isSafeUrlString(value) {
|
|
|
9
9
|
return !/^\s*(?:javascript|data|vbscript|about|livescript)\s*:/i.test(decodeURIComponent(value.replace(/[\u0000-\u001F\u007F]+/g, '')));
|
|
10
10
|
}
|
|
11
11
|
function isSafeUrlLiteral(node) {
|
|
12
|
-
|
|
13
|
-
return false;
|
|
14
|
-
}
|
|
15
|
-
if (typeof node.value !== 'string') {
|
|
16
|
-
return false;
|
|
17
|
-
}
|
|
18
|
-
return isSafeUrlString(node.value);
|
|
19
|
-
}
|
|
20
|
-
function isSafeUrlTemplate(node) {
|
|
21
|
-
if (AST_NODE_TYPES.TemplateLiteral !== node.type || 0 === node.quasis.length) {
|
|
22
|
-
return false;
|
|
23
|
-
}
|
|
24
|
-
// Basic scheme safety on the first static chunk
|
|
25
|
-
const firstChunk = node.quasis[0];
|
|
26
|
-
if (isSafeUrlLiteral(firstChunk)) {
|
|
27
|
-
return true;
|
|
28
|
-
}
|
|
29
|
-
return isUrlEncoded(node);
|
|
30
|
-
}
|
|
31
|
-
function isUrlEncoded(node) {
|
|
32
|
-
if (AST_NODE_TYPES.TemplateLiteral !== node.type) {
|
|
33
|
-
return false;
|
|
34
|
-
}
|
|
35
|
-
return Array.isArray(node.expressions) && node.expressions.length > 0 && node.expressions.every(isEncoded);
|
|
36
|
-
}
|
|
37
|
-
function isEncoded(node) {
|
|
38
|
-
if (AST_NODE_TYPES.CallExpression !== node.type) {
|
|
39
|
-
return false;
|
|
40
|
-
}
|
|
41
|
-
return AST_NODE_TYPES.Identifier === node.callee.type &&
|
|
42
|
-
('encodeURIComponent' === node.callee.name || 'encodeURI' === node.callee.name);
|
|
12
|
+
return isLiteralString(node) && isSafeUrlString(node.value);
|
|
43
13
|
}
|
|
44
14
|
function isWindowLocationAssignment(node) {
|
|
45
15
|
// window.location.<prop> = ...
|
|
@@ -60,16 +30,16 @@ function isWindowAssignment(node) {
|
|
|
60
30
|
'window' === node.left.object.name &&
|
|
61
31
|
WINDOW_PROPS.has(node.left.property.name));
|
|
62
32
|
}
|
|
63
|
-
function
|
|
33
|
+
function isWindowOrLocation(expression) {
|
|
64
34
|
// Helper to detect a window.* or window.location.*
|
|
65
|
-
if (AST_NODE_TYPES.MemberExpression !==
|
|
35
|
+
if (AST_NODE_TYPES.MemberExpression !== expression.type) {
|
|
66
36
|
return false;
|
|
67
37
|
}
|
|
68
|
-
if (AST_NODE_TYPES.Identifier ===
|
|
38
|
+
if (AST_NODE_TYPES.Identifier === expression.object.type && 'window' === expression.object.name) {
|
|
69
39
|
return true;
|
|
70
40
|
}
|
|
71
|
-
if (AST_NODE_TYPES.MemberExpression ===
|
|
72
|
-
const memberObject =
|
|
41
|
+
if (AST_NODE_TYPES.MemberExpression === expression.object.type) {
|
|
42
|
+
const memberObject = expression.object;
|
|
73
43
|
const isObjectWindow = AST_NODE_TYPES.Identifier === memberObject.object.type && 'window' === memberObject.object.name;
|
|
74
44
|
const isPropertyLocation = AST_NODE_TYPES.Identifier === memberObject.property.type && 'location' === memberObject.property.name;
|
|
75
45
|
return isObjectWindow && isPropertyLocation;
|
|
@@ -108,10 +78,7 @@ const plugin = {
|
|
|
108
78
|
if (!LOCATION_PROPS.has(propName)) {
|
|
109
79
|
return;
|
|
110
80
|
}
|
|
111
|
-
if (isSafeUrlLiteral(rhsResolved) ||
|
|
112
|
-
return;
|
|
113
|
-
}
|
|
114
|
-
if (isSanitized(rhsResolved)) {
|
|
81
|
+
if (isSafeUrlLiteral(rhsResolved) || isSanitized(rhsResolved)) {
|
|
115
82
|
return;
|
|
116
83
|
}
|
|
117
84
|
context.report({
|
|
@@ -125,13 +92,13 @@ const plugin = {
|
|
|
125
92
|
messageId: 'sanitize',
|
|
126
93
|
fix: (fixer) => {
|
|
127
94
|
const argText = context.sourceCode.getText(right);
|
|
128
|
-
return fixer.replaceText(right, `sanitize(${argText})`);
|
|
95
|
+
return fixer.replaceText(right, `sanitize( ${argText} )`);
|
|
129
96
|
},
|
|
130
97
|
}, {
|
|
131
98
|
messageId: 'domPurify',
|
|
132
99
|
fix: (fixer) => {
|
|
133
100
|
const argText = context.sourceCode.getText(right);
|
|
134
|
-
return fixer.replaceText(right, `DOMPurify.sanitize(${argText})`);
|
|
101
|
+
return fixer.replaceText(right, `DOMPurify.sanitize( ${argText} )`);
|
|
135
102
|
},
|
|
136
103
|
},
|
|
137
104
|
],
|
|
@@ -154,13 +121,13 @@ const plugin = {
|
|
|
154
121
|
messageId: 'sanitize',
|
|
155
122
|
fix: (fixer) => {
|
|
156
123
|
const argText = context.sourceCode.getText(right);
|
|
157
|
-
return fixer.replaceText(right, `sanitize(${argText})`);
|
|
124
|
+
return fixer.replaceText(right, `sanitize( ${argText} )`);
|
|
158
125
|
},
|
|
159
126
|
}, {
|
|
160
127
|
messageId: 'domPurify',
|
|
161
128
|
fix: (fixer) => {
|
|
162
129
|
const argText = context.sourceCode.getText(right);
|
|
163
|
-
return fixer.replaceText(right, `DOMPurify.sanitize(${argText})`);
|
|
130
|
+
return fixer.replaceText(right, `DOMPurify.sanitize( ${argText} )`);
|
|
164
131
|
},
|
|
165
132
|
},
|
|
166
133
|
],
|
|
@@ -173,7 +140,7 @@ const plugin = {
|
|
|
173
140
|
if (AST_NODE_TYPES.AssignmentExpression === parent.type && parent.left === node) {
|
|
174
141
|
return;
|
|
175
142
|
}
|
|
176
|
-
if (!
|
|
143
|
+
if (!isWindowOrLocation(node) || !('name' in node.property)) {
|
|
177
144
|
return;
|
|
178
145
|
}
|
|
179
146
|
const propName = node.property.name;
|
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
import { AST_NODE_TYPES, ESLintUtils } from '@typescript-eslint/utils';
|
|
2
2
|
import {} from 'typescript';
|
|
3
|
+
/**
|
|
4
|
+
* Is the node of type string.
|
|
5
|
+
* - String literals.
|
|
6
|
+
* - constants of type string.
|
|
7
|
+
* - template literals.
|
|
8
|
+
* - intrinsic type string.
|
|
9
|
+
*/
|
|
3
10
|
export function isStringLike(node, context) {
|
|
4
11
|
const type = getType(node, context);
|
|
5
12
|
const literal = type.isStringLiteral();
|
|
@@ -9,9 +16,9 @@ export function isStringLike(node, context) {
|
|
|
9
16
|
/**
|
|
10
17
|
* Get the TypeScript type of node.
|
|
11
18
|
*/
|
|
12
|
-
export function getType(
|
|
19
|
+
export function getType(expression, context) {
|
|
13
20
|
const { getTypeAtLocation } = ESLintUtils.getParserServices(context);
|
|
14
|
-
const type = getTypeAtLocation(
|
|
21
|
+
const type = getTypeAtLocation(expression);
|
|
15
22
|
return type.getNonNullableType();
|
|
16
23
|
}
|
|
17
24
|
/**
|
|
@@ -22,10 +29,13 @@ export function getType(arg, context) {
|
|
|
22
29
|
*
|
|
23
30
|
* @link https://typescript-eslint.io/developers/custom-rules/#typed-rules
|
|
24
31
|
*/
|
|
25
|
-
export function isDomElementType(
|
|
26
|
-
const
|
|
27
|
-
const name =
|
|
32
|
+
export function isDomElementType(expression, context) {
|
|
33
|
+
const type = getType(expression, context);
|
|
34
|
+
const name = type.getSymbol()?.escapedName ?? '';
|
|
28
35
|
// Match any type that ends with "Element", e.g., HTMLElement, HTMLDivElement, Element, etc.
|
|
36
|
+
if ('Element' === name) {
|
|
37
|
+
return true;
|
|
38
|
+
}
|
|
29
39
|
return name.startsWith('HTML') && name.endsWith('Element');
|
|
30
40
|
}
|
|
31
41
|
/**
|
|
@@ -45,3 +55,24 @@ export function isSanitized(node) {
|
|
|
45
55
|
}
|
|
46
56
|
return false;
|
|
47
57
|
}
|
|
58
|
+
/**
|
|
59
|
+
* Check if a node is a literal string
|
|
60
|
+
*/
|
|
61
|
+
export function isLiteralString(node) {
|
|
62
|
+
return AST_NODE_TYPES.Literal === node.type && 'string' === typeof node.value;
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Check if a node is a literal string that is safe to use in an HTML context.
|
|
66
|
+
* - Must be a literal string.
|
|
67
|
+
* - Must not contain `<script`.
|
|
68
|
+
* - Must not start with a dangerous protocol (javascript:, data:, vbscript:, about:, livescript:).
|
|
69
|
+
*/
|
|
70
|
+
export function isSafeLiteralString(node) {
|
|
71
|
+
if (!isLiteralString(node)) {
|
|
72
|
+
return false;
|
|
73
|
+
}
|
|
74
|
+
if (node.value.includes('<script')) {
|
|
75
|
+
return false;
|
|
76
|
+
}
|
|
77
|
+
return !/^\s*(?:javascript|data|vbscript|about|livescript)\s*:/i.test(decodeURIComponent(node.value.replace(/[\u0000-\u001F\u007F]+/g, '')));
|
|
78
|
+
}
|
|
@@ -1,3 +1,9 @@
|
|
|
1
|
-
import { type TSESLint } from '@typescript-eslint/utils';
|
|
1
|
+
import { type TSESLint, type TSESTree } from '@typescript-eslint/utils';
|
|
2
|
+
/**
|
|
3
|
+
* Check if an expression contains any HTML-like strings.
|
|
4
|
+
* - Looks for `<` or `>` characters in string literals and template literals.
|
|
5
|
+
* - Recursively checks binary expressions with the ` + ` operator.
|
|
6
|
+
*/
|
|
7
|
+
export declare function hasHtmlLikeStrings(node: TSESTree.Expression | TSESTree.PrivateIdentifier): boolean;
|
|
2
8
|
declare const plugin: TSESLint.RuleModule<'htmlStringConcat'>;
|
|
3
9
|
export default plugin;
|
|
@@ -1,10 +1,17 @@
|
|
|
1
1
|
import { type TSESLint, type TSESTree } from '@typescript-eslint/utils';
|
|
2
2
|
import { type Type } from 'typescript';
|
|
3
|
+
/**
|
|
4
|
+
* Is the node of type string.
|
|
5
|
+
* - String literals.
|
|
6
|
+
* - constants of type string.
|
|
7
|
+
* - template literals.
|
|
8
|
+
* - intrinsic type string.
|
|
9
|
+
*/
|
|
3
10
|
export declare function isStringLike(node: TSESTree.CallExpressionArgument, context: Readonly<TSESLint.RuleContext<string, readonly []>>): boolean;
|
|
4
11
|
/**
|
|
5
12
|
* Get the TypeScript type of node.
|
|
6
13
|
*/
|
|
7
|
-
export declare function getType<Context extends Readonly<TSESLint.RuleContext<string, readonly []>>>(
|
|
14
|
+
export declare function getType<Context extends Readonly<TSESLint.RuleContext<string, readonly []>>>(expression: TSESTree.CallExpressionArgument, context: Context): Type;
|
|
8
15
|
/**
|
|
9
16
|
* Is the type of variable being passed a DOM element?
|
|
10
17
|
*
|
|
@@ -13,9 +20,20 @@ export declare function getType<Context extends Readonly<TSESLint.RuleContext<st
|
|
|
13
20
|
*
|
|
14
21
|
* @link https://typescript-eslint.io/developers/custom-rules/#typed-rules
|
|
15
22
|
*/
|
|
16
|
-
export declare function isDomElementType<Context extends Readonly<TSESLint.RuleContext<string, readonly []>>>(
|
|
23
|
+
export declare function isDomElementType<Context extends Readonly<TSESLint.RuleContext<string, readonly []>>>(expression: TSESTree.CallExpressionArgument, context: Context): boolean;
|
|
17
24
|
/**
|
|
18
25
|
* Check if a node is a call to a known sanitization function.
|
|
19
26
|
* - Currently recognizes `sanitize(...)` and `DOMPurify.sanitize(...)`.
|
|
20
27
|
*/
|
|
21
28
|
export declare function isSanitized(node: TSESTree.Property['value'] | TSESTree.CallExpressionArgument): boolean;
|
|
29
|
+
/**
|
|
30
|
+
* Check if a node is a literal string
|
|
31
|
+
*/
|
|
32
|
+
export declare function isLiteralString(node: TSESTree.Property['value'] | TSESTree.CallExpressionArgument): node is TSESTree.StringLiteral;
|
|
33
|
+
/**
|
|
34
|
+
* Check if a node is a literal string that is safe to use in an HTML context.
|
|
35
|
+
* - Must be a literal string.
|
|
36
|
+
* - Must not contain `<script`.
|
|
37
|
+
* - Must not start with a dangerous protocol (javascript:, data:, vbscript:, about:, livescript:).
|
|
38
|
+
*/
|
|
39
|
+
export declare function isSafeLiteralString(node: TSESTree.Property['value'] | TSESTree.CallExpressionArgument): boolean;
|