@lipemat/eslint-config 5.0.1 → 5.1.0-beta.1

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.
Files changed (63) hide show
  1. package/helpers/config.js +1 -0
  2. package/helpers/config.js.map +1 -0
  3. package/{types/helpers/config.d.ts → helpers/config.ts} +16 -5
  4. package/index.js +1 -0
  5. package/package.json +8 -11
  6. package/plugins/security/helpers/dom-purify.js +1 -0
  7. package/plugins/security/helpers/dom-purify.js.map +1 -0
  8. package/plugins/security/helpers/dom-purify.ts +21 -0
  9. package/plugins/security/helpers/element.js +1 -0
  10. package/plugins/security/helpers/element.js.map +1 -0
  11. package/plugins/security/helpers/element.ts +22 -0
  12. package/plugins/security/helpers/string.js +1 -0
  13. package/plugins/security/helpers/string.js.map +1 -0
  14. package/plugins/security/helpers/string.ts +44 -0
  15. package/plugins/security/helpers/ts-types.js +1 -0
  16. package/plugins/security/helpers/ts-types.js.map +1 -0
  17. package/plugins/security/helpers/ts-types.ts +11 -0
  18. package/plugins/security/index.js +10 -0
  19. package/plugins/security/index.js.map +1 -0
  20. package/plugins/security/index.ts +74 -0
  21. package/plugins/security/package.json +25 -0
  22. package/plugins/security/rules/dangerously-set-inner-html.js +1 -0
  23. package/plugins/security/rules/dangerously-set-inner-html.js.map +1 -0
  24. package/plugins/security/rules/dangerously-set-inner-html.ts +93 -0
  25. package/plugins/security/rules/html-executing-assignment.js +1 -0
  26. package/plugins/security/rules/html-executing-assignment.js.map +1 -0
  27. package/plugins/security/rules/html-executing-assignment.ts +78 -0
  28. package/plugins/security/rules/html-executing-function.js +1 -0
  29. package/plugins/security/rules/html-executing-function.js.map +1 -0
  30. package/plugins/security/rules/html-executing-function.ts +164 -0
  31. package/plugins/security/rules/html-sinks.js +1 -0
  32. package/plugins/security/rules/html-sinks.js.map +1 -0
  33. package/plugins/security/rules/html-sinks.ts +146 -0
  34. package/plugins/security/rules/html-string-concat.js +1 -0
  35. package/plugins/security/rules/html-string-concat.js.map +1 -0
  36. package/plugins/security/rules/html-string-concat.ts +71 -0
  37. package/plugins/security/rules/jquery-executing.js +1 -0
  38. package/plugins/security/rules/jquery-executing.js.map +1 -0
  39. package/plugins/security/rules/jquery-executing.ts +134 -0
  40. package/plugins/security/rules/no-at-html-tags.js +53 -0
  41. package/plugins/security/rules/no-at-html-tags.js.map +1 -0
  42. package/plugins/security/rules/no-at-html-tags.ts +74 -0
  43. package/plugins/security/rules/vulnerable-tag-stripping.js +1 -0
  44. package/plugins/security/rules/vulnerable-tag-stripping.js.map +1 -0
  45. package/plugins/security/rules/vulnerable-tag-stripping.ts +89 -0
  46. package/plugins/security/rules/window-escaping.js +2 -1
  47. package/plugins/security/rules/window-escaping.js.map +1 -0
  48. package/plugins/security/rules/window-escaping.ts +220 -0
  49. package/plugins/tsconfig.json +8 -0
  50. package/types/index.d.ts +0 -3
  51. package/types/plugins/security/helpers/dom-purify.d.ts +0 -6
  52. package/types/plugins/security/helpers/element.d.ts +0 -10
  53. package/types/plugins/security/helpers/string.d.ts +0 -20
  54. package/types/plugins/security/helpers/ts-types.d.ts +0 -6
  55. package/types/plugins/security/index.d.ts +0 -8
  56. package/types/plugins/security/rules/dangerously-set-inner-html.d.ts +0 -4
  57. package/types/plugins/security/rules/html-executing-assignment.d.ts +0 -4
  58. package/types/plugins/security/rules/html-executing-function.d.ts +0 -6
  59. package/types/plugins/security/rules/html-sinks.d.ts +0 -4
  60. package/types/plugins/security/rules/html-string-concat.d.ts +0 -9
  61. package/types/plugins/security/rules/jquery-executing.d.ts +0 -17
  62. package/types/plugins/security/rules/vulnerable-tag-stripping.d.ts +0 -4
  63. package/types/plugins/security/rules/window-escaping.d.ts +0 -5
@@ -0,0 +1,164 @@
1
+ import {AST_NODE_TYPES, type TSESLint, type TSESTree} from '@typescript-eslint/utils';
2
+ import {isJQueryCall} from './jquery-executing.js';
3
+ import {isSafeLiteralString} from '../helpers/string.js';
4
+ import {isSanitized} from '../helpers/dom-purify.js';
5
+ import {isDomElementType} from '../helpers/element.js';
6
+
7
+ type HtmlExecutingFunctions = 'document.write' | 'document.writeln';
8
+
9
+ type UnsafeCalls =
10
+ 'after'
11
+ | 'append'
12
+ | 'before'
13
+ | 'insertAdjacentHTML'
14
+ | 'prepend'
15
+ | 'replaceWith'
16
+ | 'setAttribute';
17
+
18
+
19
+ type Messages = HtmlExecutingFunctions | UnsafeCalls | 'sanitize' | 'domPurify';
20
+ type Context = TSESLint.RuleContext<Messages, []>;
21
+
22
+
23
+ const DOCUMENT_METHODS: HtmlExecutingFunctions[] = [
24
+ 'document.write',
25
+ 'document.writeln',
26
+ ];
27
+
28
+ const SECOND_ARG_METHODS = new Set<UnsafeCalls>( [
29
+ 'insertAdjacentHTML',
30
+ 'setAttribute',
31
+ ] );
32
+
33
+ const UNSAFE_METHODS = [
34
+ 'after', 'append', 'before', 'insertAdjacentHTML', 'prepend', 'replaceWith', 'setAttribute',
35
+ ] as const satisfies readonly UnsafeCalls[];
36
+
37
+ function isDocumentMethod( methodName: string ): methodName is HtmlExecutingFunctions {
38
+ return DOCUMENT_METHODS.includes( methodName as HtmlExecutingFunctions );
39
+ }
40
+
41
+ function isUnsafeMethod( methodName: string ): methodName is UnsafeCalls {
42
+ return UNSAFE_METHODS.includes( methodName as UnsafeCalls );
43
+ }
44
+
45
+ function isSecondArgMethod( methodName: string ): methodName is UnsafeCalls {
46
+ return SECOND_ARG_METHODS.has( methodName as UnsafeCalls );
47
+ }
48
+
49
+
50
+ function getDocumentCall( node: TSESTree.CallExpression ): HtmlExecutingFunctions | null {
51
+ let calleeName = '';
52
+ if ( AST_NODE_TYPES.Identifier === node.callee.type ) {
53
+ calleeName = node.callee.name;
54
+ } else if ( AST_NODE_TYPES.MemberExpression === node.callee.type ) {
55
+ if ( 'name' in node.callee.object ) {
56
+ calleeName = node.callee.object.name;
57
+ if ( 'name' in node.callee.property ) {
58
+ calleeName += '.' + node.callee.property.name;
59
+ }
60
+ } else if ( 'name' in node.callee.property ) {
61
+ calleeName = node.callee.property.name;
62
+ }
63
+ }
64
+ if ( isDocumentMethod( calleeName ) ) {
65
+ return calleeName;
66
+ }
67
+
68
+ return null;
69
+ }
70
+
71
+
72
+ function getElementMethodCall( node: TSESTree.CallExpression, context: Context ): UnsafeCalls | null {
73
+ if ( AST_NODE_TYPES.MemberExpression !== node.callee.type || AST_NODE_TYPES.Identifier !== node.callee.property.type ) {
74
+ return null;
75
+ }
76
+ const methodName = node.callee.property.name;
77
+ if ( ! isDomElementType( node.callee.object, context ) ) {
78
+ return null; // We only care about DOM element method calls.
79
+ }
80
+
81
+ if ( ! isUnsafeMethod( methodName ) ) {
82
+ return null;
83
+ }
84
+ if ( isJQueryCall( node ) ) {
85
+ return null; // Handled in jquery-executing rule
86
+ }
87
+ return methodName;
88
+ }
89
+
90
+
91
+ const plugin: TSESLint.RuleModule<Messages> = {
92
+ defaultOptions: [],
93
+ meta: {
94
+ docs: {
95
+ description: 'Disallow using unsanitized values in functions that execute HTML',
96
+ },
97
+ hasSuggestions: true,
98
+ messages: {
99
+ 'document.write': 'Any HTML used with `document.write` gets executed. Make sure it\'s properly escaped.',
100
+ 'document.writeln': 'Any HTML used with `document.writeln` gets executed. Make sure it\'s properly escaped.',
101
+ after: 'Any HTML used with `after` gets executed. Make sure it\'s properly escaped.',
102
+ append: 'Any HTML used with `append` gets executed. Make sure it\'s properly escaped.',
103
+ before: 'Any HTML used with `before` gets executed. Make sure it\'s properly escaped.',
104
+ insertAdjacentHTML: 'Any HTML used with `insertAdjacentHTML` gets executed. Make sure it\'s properly escaped.',
105
+ prepend: 'Any HTML used with `prepend` gets executed. Make sure it\'s properly escaped.',
106
+ replaceWith: 'Any HTML used with `replaceWith` gets executed. Make sure it\'s properly escaped.',
107
+ setAttribute: 'Any HTML used with `setAttribute` can lead to XSS. Make sure it\'s properly escaped.',
108
+
109
+ // Suggestions
110
+ domPurify: 'Wrap the argument with a `DOMPurify.sanitize()` call.',
111
+ sanitize: 'Wrap the argument with a `sanitize()` call.',
112
+ },
113
+ schema: [],
114
+ type: 'problem',
115
+
116
+ },
117
+ create( context: Context ): TSESLint.RuleListener {
118
+ return {
119
+ CallExpression( node: TSESTree.CallExpression ) {
120
+ let method: HtmlExecutingFunctions | UnsafeCalls | null;
121
+ const documentMethod = getDocumentCall( node );
122
+
123
+ if ( null !== documentMethod ) {
124
+ method = documentMethod;
125
+ } else {
126
+ method = getElementMethodCall( node, context );
127
+ if ( null === method ) {
128
+ return;
129
+ }
130
+ }
131
+
132
+ let arg: TSESTree.CallExpressionArgument = node.arguments[ 0 ];
133
+ if ( isSecondArgMethod( method ) ) {
134
+ arg = node.arguments[ 1 ];
135
+ }
136
+
137
+ if ( ! isSafeLiteralString( arg ) && ! isSanitized( arg ) && ! isDomElementType<Context>( arg, context ) ) {
138
+ context.report( {
139
+ node,
140
+ messageId: method,
141
+ suggest: [
142
+ {
143
+ messageId: 'domPurify',
144
+ fix: ( fixer: TSESLint.RuleFixer ) => {
145
+ const argText = context.sourceCode.getText( arg );
146
+ return fixer.replaceText( arg, `DOMPurify.sanitize( ${argText} )` );
147
+ },
148
+ },
149
+ {
150
+ messageId: 'sanitize',
151
+ fix: ( fixer: TSESLint.RuleFixer ) => {
152
+ const argText = context.sourceCode.getText( arg );
153
+ return fixer.replaceText( arg, `sanitize( ${argText} )` );
154
+ },
155
+ },
156
+ ],
157
+ } );
158
+ }
159
+ },
160
+ };
161
+ },
162
+ };
163
+
164
+ export default plugin;
@@ -121,3 +121,4 @@ const plugin = {
121
121
  },
122
122
  };
123
123
  export default plugin;
124
+ //# sourceMappingURL=html-sinks.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"html-sinks.js","sourceRoot":"","sources":["html-sinks.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,146 @@
1
+ import {AST_NODE_TYPES, type TSESLint, type TSESTree} from '@typescript-eslint/utils';
2
+ import {isLiteralString, isStringLike} from '../helpers/string.js';
3
+ import {isSanitized} from '../helpers/dom-purify.js';
4
+
5
+ type Messages =
6
+ 'setTimeoutString'
7
+ | 'setIntervalString'
8
+ | 'windowOpenUnsanitized'
9
+ | 'cssTextUnsanitized'
10
+ | 'sanitize'
11
+ | 'domPurify';
12
+
13
+ type Context = TSESLint.RuleContext<Messages, []>;
14
+
15
+ /**
16
+ * Get the callee name from a call expression
17
+ */
18
+ function getCalleeName( node: TSESTree.CallExpression ): string {
19
+ if ( AST_NODE_TYPES.Identifier === node.callee.type ) {
20
+ return node.callee.name;
21
+ } else if ( AST_NODE_TYPES.MemberExpression === node.callee.type ) {
22
+ if ( 'name' in node.callee.object && 'name' in node.callee.property ) {
23
+ return `${node.callee.object.name}.${node.callee.property.name}`;
24
+ } else if ( 'name' in node.callee.property ) {
25
+ return node.callee.property.name;
26
+ }
27
+ }
28
+ return '';
29
+ }
30
+
31
+ /**
32
+ * Check if the assignment is to body.style.cssText
33
+ */
34
+ function isCssTextAssignment( node: TSESTree.AssignmentExpression ): boolean {
35
+ if ( AST_NODE_TYPES.MemberExpression !== node.left.type ) {
36
+ return false;
37
+ }
38
+
39
+ const memberExpr = node.left;
40
+ if ( AST_NODE_TYPES.MemberExpression !== memberExpr.object.type || ! ( 'name' in memberExpr.property ) ) {
41
+ return false;
42
+ }
43
+
44
+ const parentMember = memberExpr.object;
45
+ if ( ! ( 'name' in parentMember.object ) || ! ( 'name' in parentMember.property ) ) {
46
+ return false;
47
+ }
48
+
49
+ return (
50
+ 'body' === parentMember.object.name &&
51
+ 'style' === parentMember.property.name &&
52
+ 'cssText' === memberExpr.property.name
53
+ );
54
+ }
55
+
56
+ const plugin: TSESLint.RuleModule<Messages> = {
57
+ defaultOptions: [],
58
+ meta: {
59
+ type: 'problem',
60
+ docs: {
61
+ description: 'Detect security issues with HTML sinks: setTimeout/setInterval with strings, unsanitized window.open, and unsanitized body.style.cssText',
62
+ },
63
+ messages: {
64
+ setTimeoutString: 'setTimeout should not receive a string. Pass a function instead.',
65
+ setIntervalString: 'setInterval should not receive a string. Pass a function instead.',
66
+ windowOpenUnsanitized: 'window.open should be sanitized to prevent XSS attacks.',
67
+ cssTextUnsanitized: 'body.style.cssText should be sanitized when not a literal string to prevent XSS attacks.',
68
+ sanitize: 'Wrap with sanitize(...)',
69
+ domPurify: 'Wrap with DOMPurify.sanitize(...)',
70
+ },
71
+ schema: [],
72
+ hasSuggestions: true,
73
+ },
74
+
75
+ create( context: Context ): TSESLint.RuleListener {
76
+ return {
77
+ CallExpression( node: TSESTree.CallExpression ) {
78
+ const calleeName = getCalleeName( node );
79
+
80
+ // Handle setTimeout and setInterval with string arguments (not allowed)
81
+ if ( 'setTimeout' === calleeName || 'setInterval' === calleeName ) {
82
+ const firstArg = node.arguments[ 0 ];
83
+ if ( isStringLike( firstArg, context ) ) {
84
+ context.report( {
85
+ node,
86
+ messageId: 'setTimeout' === calleeName ? 'setTimeoutString' : 'setIntervalString',
87
+ } );
88
+ }
89
+ return;
90
+ }
91
+
92
+ // Handle window.open with string arguments (must be sanitized)
93
+ if ( 'window.open' === calleeName ) {
94
+ const firstArg = node.arguments[ 0 ];
95
+ if ( ! isSanitized( firstArg ) ) {
96
+ const sourceCode = context.sourceCode;
97
+ const argText = sourceCode.getText( firstArg );
98
+
99
+ context.report( {
100
+ node,
101
+ messageId: 'windowOpenUnsanitized',
102
+ suggest: [
103
+ {
104
+ messageId: 'domPurify',
105
+ fix: fixer => fixer.replaceText( firstArg, `DOMPurify.sanitize( ${argText} )` ),
106
+ },
107
+ {
108
+ messageId: 'sanitize',
109
+ fix: fixer => fixer.replaceText( firstArg, `sanitize( ${argText} )` ),
110
+ },
111
+ ],
112
+ } );
113
+ }
114
+ }
115
+ },
116
+
117
+ AssignmentExpression( node: TSESTree.AssignmentExpression ) {
118
+ // Handle body.style.cssText assignments
119
+ if ( isCssTextAssignment( node ) ) {
120
+ // Allow literal strings but require sanitization for other values
121
+ if ( ! isLiteralString( node.right ) && ! isSanitized( node.right ) ) {
122
+ const sourceCode = context.sourceCode;
123
+ const rightText = sourceCode.getText( node.right );
124
+
125
+ context.report( {
126
+ node,
127
+ messageId: 'cssTextUnsanitized',
128
+ suggest: [
129
+ {
130
+ messageId: 'domPurify',
131
+ fix: fixer => fixer.replaceText( node.right, `DOMPurify.sanitize( ${rightText} )` ),
132
+ },
133
+ {
134
+ messageId: 'sanitize',
135
+ fix: fixer => fixer.replaceText( node.right, `sanitize( ${rightText} )` ),
136
+ },
137
+ ],
138
+ } );
139
+ }
140
+ }
141
+ },
142
+ };
143
+ },
144
+ };
145
+
146
+ export default plugin;
@@ -61,3 +61,4 @@ const plugin = {
61
61
  },
62
62
  };
63
63
  export default plugin;
64
+ //# sourceMappingURL=html-string-concat.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"html-string-concat.js","sourceRoot":"","sources":["html-string-concat.ts"],"names":[],"mappings":"AAAA,OAAO,EAAC,cAAc,EAA+B,MAAM,0BAA0B,CAAC;AAItF,SAAS,cAAc,CAAE,IAAqC;IAC7D,6CAA6C;IAC7C,OAAO,cAAc,CAAC,gBAAgB,KAAK,IAAI,CAAC,IAAI,IAAI,GAAG,KAAK,IAAI,CAAC,QAAQ,IAAI,kBAAkB,CAAE,IAAI,CAAE,CAAC;AAC7G,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,kBAAkB,CAAE,IAAsD;IACzF,IAAK,cAAc,CAAC,OAAO,KAAK,IAAI,CAAC,IAAI,IAAI,QAAQ,KAAK,OAAO,IAAI,CAAC,KAAK,EAAG,CAAC;QAC9E,OAAO,MAAM,CAAC,IAAI,CAAE,IAAI,CAAC,KAAK,CAAE,CAAC;IAClC,CAAC;IACD,IAAK,cAAc,CAAC,eAAe,KAAK,IAAI,CAAC,IAAI,EAAG,CAAC;QACpD,+DAA+D;QAC/D,OAAO,IAAI,CAAC,MAAM,CAAC,IAAI,CAAE,CAAC,CAAC,EAAE,CAAC,MAAM,CAAC,IAAI,CAAE,CAAC,CAAC,KAAK,CAAC,MAAM,CAAE,CAAE,CAAC;IAC/D,CAAC;IACD,IAAK,cAAc,CAAC,gBAAgB,KAAK,IAAI,CAAC,IAAI,IAAI,GAAG,KAAK,IAAI,CAAC,QAAQ,EAAG,CAAC;QAC9E,OAAO,kBAAkB,CAAE,IAAI,CAAC,IAAI,CAAE,IAAI,kBAAkB,CAAE,IAAI,CAAC,KAAK,CAAE,CAAC;IAC5E,CAAC;IACD,OAAO,KAAK,CAAC;AACd,CAAC;AAGD,MAAM,MAAM,GAA4C;IACvD,cAAc,EAAE,EAAE;IAClB,IAAI,EAAE;QACL,IAAI,EAAE,SAAS;QACf,IAAI,EAAE;YACL,WAAW,EAAE,sDAAsD;SACnE;QACD,QAAQ,EAAE;YACT,gBAAgB,EAAE,0HAA0H;SAC5I;QACD,MAAM,EAAE,EAAE;KACV;IACD,MAAM,CAAE,OAAgB;QACvB,OAAO;YACN,oBAAoB,CAAE,IAAmC;gBACxD,MAAM,KAAK,GAAwB,IAAI,CAAC,KAAK,CAAC;gBAC9C,IAAK,cAAc,CAAE,KAAK,CAAE,EAAG,CAAC;oBAC/B,OAAO,CAAC,MAAM,CAAE;wBACf,IAAI;wBACJ,SAAS,EAAE,kBAAkB;qBAC7B,CAAE,CAAC;gBACL,CAAC;YACF,CAAC;YAED,kBAAkB,CAAE,IAAiC;gBACpD,2DAA2D;gBAC3D,MAAM,IAAI,GAAG,IAAI,CAAC,IAAI,CAAC;gBACvB,IAAK,IAAI,KAAK,IAAI,EAAG,CAAC;oBACrB,OAAO;gBACR,CAAC;gBACD,IAAK,cAAc,CAAE,IAAI,CAAE,EAAG,CAAC;oBAC9B,OAAO,CAAC,MAAM,CAAE;wBACf,IAAI;wBACJ,SAAS,EAAE,kBAAkB;qBAC7B,CAAE,CAAC;gBACL,CAAC;YACF,CAAC;SACD,CAAC;IACH,CAAC;CACD,CAAC;AAEF,eAAe,MAAM,CAAC"}
@@ -0,0 +1,71 @@
1
+ import {AST_NODE_TYPES, type TSESLint, type TSESTree} from '@typescript-eslint/utils';
2
+
3
+ type Context = TSESLint.RuleContext<'htmlStringConcat', []>;
4
+
5
+ function isStringConcat( node: TSESTree.CallExpressionArgument ): boolean {
6
+ // 'foo' + userInput + 'bar' (HTML-like only)
7
+ return AST_NODE_TYPES.BinaryExpression === node.type && '+' === node.operator && hasHtmlLikeStrings( node );
8
+ }
9
+
10
+ /**
11
+ * Check if an expression contains any HTML-like strings.
12
+ * - Looks for `<` or `>` characters in string literals and template literals.
13
+ * - Recursively checks binary expressions with the ` + ` operator.
14
+ */
15
+ export function hasHtmlLikeStrings( node: TSESTree.Expression | TSESTree.PrivateIdentifier ): boolean {
16
+ if ( AST_NODE_TYPES.Literal === node.type && 'string' === typeof node.value ) {
17
+ return /[<>]/.test( node.value );
18
+ }
19
+ if ( AST_NODE_TYPES.TemplateLiteral === node.type ) {
20
+ // Check any static part of the template for HTML-like content.
21
+ return node.quasis.some( q => /[<>]/.test( q.value.cooked ) );
22
+ }
23
+ if ( AST_NODE_TYPES.BinaryExpression === node.type && '+' === node.operator ) {
24
+ return hasHtmlLikeStrings( node.left ) || hasHtmlLikeStrings( node.right );
25
+ }
26
+ return false;
27
+ }
28
+
29
+
30
+ const plugin: TSESLint.RuleModule<'htmlStringConcat'> = {
31
+ defaultOptions: [],
32
+ meta: {
33
+ type: 'problem',
34
+ docs: {
35
+ description: 'Disallow string concatenation with HTML-like content',
36
+ },
37
+ messages: {
38
+ htmlStringConcat: 'HTML string concatenation detected, this is a security risk, use DOM node construction or a templating language instead.',
39
+ },
40
+ schema: [],
41
+ },
42
+ create( context: Context ): TSESLint.RuleListener {
43
+ return {
44
+ AssignmentExpression( node: TSESTree.AssignmentExpression ) {
45
+ const right: TSESTree.Expression = node.right;
46
+ if ( isStringConcat( right ) ) {
47
+ context.report( {
48
+ node,
49
+ messageId: 'htmlStringConcat',
50
+ } );
51
+ }
52
+ },
53
+
54
+ VariableDeclarator( node: TSESTree.VariableDeclarator ) {
55
+ // Detect string concatenation assigned at declaration time
56
+ const init = node.init;
57
+ if ( null === init ) {
58
+ return;
59
+ }
60
+ if ( isStringConcat( init ) ) {
61
+ context.report( {
62
+ node,
63
+ messageId: 'htmlStringConcat',
64
+ } );
65
+ }
66
+ },
67
+ };
68
+ },
69
+ };
70
+
71
+ export default plugin;
@@ -103,3 +103,4 @@ const plugin = {
103
103
  },
104
104
  };
105
105
  export default plugin;
106
+ //# sourceMappingURL=jquery-executing.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"jquery-executing.js","sourceRoot":"","sources":["jquery-executing.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,134 @@
1
+ import {AST_NODE_TYPES, type TSESLint, type TSESTree} from '@typescript-eslint/utils';
2
+ import type {Type} from 'typescript';
3
+ import {isSanitized} from '../helpers/dom-purify.js';
4
+ import {getType} from '../helpers/ts-types.js';
5
+
6
+ type UnsafeCalls =
7
+ 'after'
8
+ | 'append'
9
+ | 'appendTo'
10
+ | 'before'
11
+ | 'html'
12
+ | 'insertAfter'
13
+ | 'insertBefore'
14
+ | 'prepend'
15
+ | 'prependTo'
16
+ | 'replaceAll'
17
+ | 'replaceWith';
18
+
19
+ type Messages = 'needsEscaping' | 'sanitize' | 'domPurify';
20
+ type Context = TSESLint.RuleContext<Messages, []>;
21
+
22
+
23
+ /**
24
+ * @link https://docs.wpvip.com/security/javascript-security-recommendations/#h-stripping-tags
25
+ */
26
+ const JQUERY_METHODS: UnsafeCalls[] = [
27
+ 'after', 'append', 'appendTo', 'before', 'html',
28
+ 'insertAfter', 'insertBefore', 'prepend', 'prependTo',
29
+ 'replaceAll', 'replaceWith',
30
+ ];
31
+
32
+ /**
33
+ * Is the type of variable being passed a jQuery element?
34
+ *
35
+ * - jQuery elements are of type `JQuery`.
36
+ * - jQuery elements do not require sanitization.
37
+ *
38
+ * @link https://typescript-eslint.io/developers/custom-rules/#typed-rules
39
+ */
40
+ export function isJQueryElementType( arg: TSESTree.CallExpressionArgument, context: Context ): boolean {
41
+ const element: Type = getType<Context>( arg, context );
42
+ return 'JQuery' === element.getSymbol()?.escapedName;
43
+ }
44
+
45
+
46
+ function isJQueryMethod( methodName: string ): methodName is UnsafeCalls {
47
+ return JQUERY_METHODS.includes( methodName as UnsafeCalls );
48
+ }
49
+
50
+ export function isJQueryCall( node: TSESTree.CallExpression ): boolean {
51
+ if ( AST_NODE_TYPES.MemberExpression !== node.callee.type || ! ( 'name' in node.callee.property ) ) {
52
+ return false;
53
+ }
54
+ // Walk to the root object of the call chain
55
+ let obj = node.callee.object ?? null;
56
+ if ( null === obj ) {
57
+ return false;
58
+ }
59
+ while ( AST_NODE_TYPES.MemberExpression === obj.type ) {
60
+ obj = obj.object;
61
+ }
62
+ return ( AST_NODE_TYPES.CallExpression === obj.type && AST_NODE_TYPES.Identifier === obj.callee.type &&
63
+ ( '$' === obj.callee.name || 'jQuery' === obj.callee.name )
64
+ );
65
+ }
66
+
67
+
68
+ export function getJQueryCall( node: TSESTree.CallExpression ): UnsafeCalls | null {
69
+ // Detect $(...).method(userInput) or jQuery(...).method(...)
70
+ if ( AST_NODE_TYPES.MemberExpression !== node.callee.type || ! ( 'name' in node.callee.property ) ) {
71
+ return null;
72
+ }
73
+ const methodName = node.callee.property.name;
74
+ if ( ! isJQueryMethod( methodName ) ) {
75
+ return null;
76
+ }
77
+ return isJQueryCall( node ) ? methodName : null;
78
+ }
79
+
80
+ const plugin: TSESLint.RuleModule<Messages> = {
81
+ defaultOptions: [],
82
+ meta: {
83
+ docs: {
84
+ description: 'Disallow using unsanitized values in jQuery methods that execute HTML',
85
+ },
86
+ hasSuggestions: true,
87
+ messages: {
88
+ needsEscaping: 'Any HTML used with `{{methodName}}` gets executed. Make sure it\'s properly escaped.',
89
+
90
+ // Suggestions
91
+ domPurify: 'Wrap the argument with a `DOMPurify.sanitize()` call.',
92
+ sanitize: 'Wrap the argument with a `sanitize()` call.',
93
+ },
94
+ schema: [],
95
+ type: 'problem',
96
+ },
97
+ create( context: Context ): TSESLint.RuleListener {
98
+ return {
99
+ CallExpression( node: TSESTree.CallExpression ) {
100
+ const methodName = getJQueryCall( node );
101
+ if ( null !== methodName ) {
102
+ const arg: TSESTree.CallExpressionArgument = node.arguments[ 0 ];
103
+ if ( ! isSanitized( arg ) && ! isJQueryElementType( arg, context ) ) {
104
+ context.report( {
105
+ node,
106
+ messageId: 'needsEscaping',
107
+ data: {
108
+ methodName,
109
+ },
110
+ suggest: [
111
+ {
112
+ messageId: 'domPurify',
113
+ fix: ( fixer: TSESLint.RuleFixer ) => {
114
+ const argText = context.sourceCode.getText( arg );
115
+ return fixer.replaceText( arg, `DOMPurify.sanitize( ${argText} )` );
116
+ },
117
+ },
118
+ {
119
+ messageId: 'sanitize',
120
+ fix: ( fixer: TSESLint.RuleFixer ) => {
121
+ const argText = context.sourceCode.getText( arg );
122
+ return fixer.replaceText( arg, `sanitize( ${argText} )` );
123
+ },
124
+ },
125
+ ],
126
+ } );
127
+ }
128
+ }
129
+ },
130
+ };
131
+ },
132
+ };
133
+
134
+ export default plugin;
@@ -0,0 +1,53 @@
1
+ import {} from '@typescript-eslint/utils';
2
+ import { isSanitized } from '../helpers/dom-purify.js';
3
+ const plugin = {
4
+ defaultOptions: [],
5
+ meta: {
6
+ docs: {
7
+ description: 'disallow using `{@html}` to prevent XSS attack. Make sure it\'s properly escaped.',
8
+ },
9
+ schema: [],
10
+ messages: {
11
+ dangerousHtml: '`{@html}` can lead to XSS attack.',
12
+ // Suggestions
13
+ domPurify: 'Wrap the content with a `DOMPurify.sanitize()` call.',
14
+ sanitize: 'Wrap the content with a `sanitize()` call.',
15
+ },
16
+ hasSuggestions: true,
17
+ type: 'problem',
18
+ },
19
+ create(context) {
20
+ return {
21
+ 'SvelteMustacheTag[kind=raw]'(node) {
22
+ if (isSanitized(node.expression)) {
23
+ return;
24
+ }
25
+ const expression = node.expression;
26
+ context.report({
27
+ node,
28
+ messageId: 'dangerousHtml',
29
+ suggest: [
30
+ {
31
+ messageId: 'domPurify',
32
+ fix: (fixer) => {
33
+ const argText = context.sourceCode.getText(expression);
34
+ // @ts-expect-error - TS2345: Not a node, but has all required Node properties.
35
+ return fixer.replaceText(node, `{@html DOMPurify.sanitize( ${argText} )}`);
36
+ },
37
+ },
38
+ {
39
+ messageId: 'sanitize',
40
+ fix: (fixer) => {
41
+ const argText = context.sourceCode.getText(expression);
42
+ // @ts-expect-error - TS2345: Not a node, but has all required Node properties.
43
+ return fixer.replaceText(node, `{@html sanitize( ${argText} )}`);
44
+ },
45
+ },
46
+ ],
47
+ });
48
+ },
49
+ };
50
+ },
51
+ };
52
+ export default plugin;
53
+ //# sourceMappingURL=no-at-html-tags.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"no-at-html-tags.js","sourceRoot":"","sources":["no-at-html-tags.ts"],"names":[],"mappings":"AAAA,OAAO,EAA8B,MAAM,0BAA0B,CAAC;AAEtE,OAAO,EAAC,WAAW,EAAC,MAAM,0BAA0B,CAAC;AAmBrD,MAAM,MAAM,GAAW;IACtB,cAAc,EAAE,EAAE;IAClB,IAAI,EAAE;QACL,IAAI,EAAE;YACL,WAAW,EAAE,mFAAmF;SAChG;QACD,MAAM,EAAE,EAAE;QACV,QAAQ,EAAE;YACT,aAAa,EAAE,mCAAmC;YAElD,cAAc;YACd,SAAS,EAAE,sDAAsD;YACjE,QAAQ,EAAE,4CAA4C;SACtD;QACD,cAAc,EAAE,IAAI;QACpB,IAAI,EAAE,SAAS;KACf;IACD,MAAM,CAAE,OAAsB;QAC7B,OAAO;YACN,6BAA6B,CAAE,IAA2B;gBACzD,IAAK,WAAW,CAAE,IAAI,CAAC,UAAU,CAAE,EAAG,CAAC;oBACtC,OAAO;gBACR,CAAC;gBACD,MAAM,UAAU,GAAkB,IAAI,CAAC,UAA2B,CAAC;gBAEnE,OAAO,CAAC,MAAM,CAAE;oBACf,IAAI;oBACJ,SAAS,EAAE,eAAe;oBAC1B,OAAO,EAAE;wBACR;4BACC,SAAS,EAAE,WAAW;4BACtB,GAAG,EAAE,CAAE,KAAyB,EAAG,EAAE;gCACpC,MAAM,OAAO,GAAG,OAAO,CAAC,UAAU,CAAC,OAAO,CAAE,UAAU,CAAE,CAAC;gCACzD,+EAA+E;gCAC/E,OAAO,KAAK,CAAC,WAAW,CAAE,IAAI,EAAE,8BAA8B,OAAO,KAAK,CAAE,CAAC;4BAC9E,CAAC;yBACD;wBACD;4BACC,SAAS,EAAE,UAAU;4BACrB,GAAG,EAAE,CAAE,KAAyB,EAAG,EAAE;gCACpC,MAAM,OAAO,GAAG,OAAO,CAAC,UAAU,CAAC,OAAO,CAAE,UAAU,CAAE,CAAC;gCACzD,+EAA+E;gCAC/E,OAAO,KAAK,CAAC,WAAW,CAAE,IAAI,EAAE,oBAAoB,OAAO,KAAK,CAAE,CAAC;4BACpE,CAAC;yBACD;qBACD;iBACD,CAAE,CAAC;YACL,CAAC;SACD,CAAC;IACH,CAAC;CACD,CAAC;AAEF,eAAe,MAAM,CAAC"}