@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,74 @@
1
+ import {type TSESLint, type TSESTree} from '@typescript-eslint/utils';
2
+ import type {AST} from 'svelte-eslint-parser';
3
+ import {isSanitized} from '../helpers/dom-purify.js';
4
+
5
+
6
+ type MessageIds = 'dangerousHtml' | 'domPurify' | 'sanitize';
7
+
8
+ type RuleOptions = Exclude<TSESLint.ReportDescriptor<MessageIds>, 'node'> | {
9
+ node: AST.SvelteMustacheTag,
10
+ readonly messageId: MessageIds;
11
+ readonly suggest?: Readonly<TSESLint.ReportSuggestionArray<MessageIds>> | null;
12
+ }
13
+
14
+ interface SvelteContext extends TSESLint.RuleContext<MessageIds, []> {
15
+ report( options: RuleOptions ): void;
16
+ }
17
+
18
+ interface Module extends TSESLint.RuleModule<MessageIds> {
19
+ create( context: SvelteContext ): TSESLint.RuleListener;
20
+ }
21
+
22
+ const plugin: Module = {
23
+ defaultOptions: [],
24
+ meta: {
25
+ docs: {
26
+ description: 'disallow using `{@html}` to prevent XSS attack. Make sure it\'s properly escaped.',
27
+ },
28
+ schema: [],
29
+ messages: {
30
+ dangerousHtml: '`{@html}` can lead to XSS attack.',
31
+
32
+ // Suggestions
33
+ domPurify: 'Wrap the content with a `DOMPurify.sanitize()` call.',
34
+ sanitize: 'Wrap the content with a `sanitize()` call.',
35
+ },
36
+ hasSuggestions: true,
37
+ type: 'problem',
38
+ },
39
+ create( context: SvelteContext ) {
40
+ return {
41
+ 'SvelteMustacheTag[kind=raw]'( node: AST.SvelteMustacheTag ) {
42
+ if ( isSanitized( node.expression ) ) {
43
+ return;
44
+ }
45
+ const expression: TSESTree.Node = node.expression as TSESTree.Node;
46
+
47
+ context.report( {
48
+ node,
49
+ messageId: 'dangerousHtml',
50
+ suggest: [
51
+ {
52
+ messageId: 'domPurify',
53
+ fix: ( fixer: TSESLint.RuleFixer ) => {
54
+ const argText = context.sourceCode.getText( expression );
55
+ // @ts-expect-error - TS2345: Not a node, but has all required Node properties.
56
+ return fixer.replaceText( node, `{@html DOMPurify.sanitize( ${argText} )}` );
57
+ },
58
+ },
59
+ {
60
+ messageId: 'sanitize',
61
+ fix: ( fixer: TSESLint.RuleFixer ) => {
62
+ const argText = context.sourceCode.getText( expression );
63
+ // @ts-expect-error - TS2345: Not a node, but has all required Node properties.
64
+ return fixer.replaceText( node, `{@html sanitize( ${argText} )}` );
65
+ },
66
+ },
67
+ ],
68
+ } );
69
+ },
70
+ };
71
+ },
72
+ };
73
+
74
+ export default plugin;
@@ -73,3 +73,4 @@ const plugin = {
73
73
  },
74
74
  };
75
75
  export default plugin;
76
+ //# sourceMappingURL=vulnerable-tag-stripping.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"vulnerable-tag-stripping.js","sourceRoot":"","sources":["vulnerable-tag-stripping.ts"],"names":[],"mappings":"AAAA,OAAO,EAAC,cAAc,EAA+B,MAAM,0BAA0B,CAAC;AACtF,OAAO,EAAC,YAAY,EAAC,MAAM,uBAAuB,CAAC;AAMnD;;;;;;GAMG;AACH,SAAS,eAAe,CAAE,IAA6B;IACtD,kCAAkC;IAClC,IAAK,cAAc,CAAC,gBAAgB,KAAK,IAAI,CAAC,MAAM,CAAC,IAAI,IAAI,CAAE,CAAE,MAAM,IAAI,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAE,EAAG,CAAC;QACpG,OAAO,IAAI,CAAC;IACb,CAAC;IAED,IAAK,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,IAAI,KAAK,MAAM,EAAG,CAAC;QAC5C,OAAO,IAAI,CAAC;IACb,CAAC;IAED,MAAM,UAAU,GAAG,IAAI,CAAC,MAAM,CAAC,MAAM,CAAC;IACtC,IAAK,cAAc,CAAC,cAAc,KAAK,UAAU,CAAC,IAAI,EAAG,CAAC;QACzD,OAAO,IAAI,CAAC;IACb,CAAC;IAED,IAAK,cAAc,CAAC,gBAAgB,KAAK,UAAU,CAAC,MAAM,CAAC,IAAI,IAAI,CAAE,CAAE,MAAM,IAAI,UAAU,CAAC,MAAM,CAAC,QAAQ,CAAE,EAAG,CAAC;QAChH,OAAO,IAAI,CAAC;IACb,CAAC;IAED,IAAK,UAAU,CAAC,MAAM,CAAC,QAAQ,CAAC,IAAI,KAAK,MAAM,EAAG,CAAC;QAClD,OAAO,IAAI,CAAC;IACb,CAAC;IAED,OAAO,YAAY,CAAE,UAAU,CAAE,CAAC,CAAC,CAAC,UAAU,CAAC,MAAM,CAAC,CAAC,CAAC,IAAI,CAAC;AAC9D,CAAC;AAGD,MAAM,MAAM,GAAkC;IAC7C,cAAc,EAAE,EAAE;IAClB,IAAI,EAAE;QACL,IAAI,EAAE;YACL,WAAW,EAAE,qFAAqF;SAClG;QACD,cAAc,EAAE,IAAI;QACpB,QAAQ,EAAE;YACT,sBAAsB,EAAE,8FAA8F;YACtH,WAAW,EAAE,iDAAiD;SAC9D;QACD,MAAM,EAAE,EAAE;QACV,IAAI,EAAE,SAAS;KACf;IACD,MAAM,CAAE,OAAgB;QACvB,OAAO;YACN,cAAc,CAAE,IAA6B;gBAC5C,MAAM,YAAY,GAAG,eAAe,CAAE,IAAI,CAAE,CAAC;gBAC7C,IAAK,IAAI,KAAK,YAAY,EAAG,CAAC;oBAC7B,OAAO;gBACR,CAAC;gBACD,MAAM,cAAc,GAAG,YAAY,CAAC,MAAM,CAAC;gBAC3C,MAAM,UAAU,GAAG,YAAY,CAAC,MAAM,CAAC;gBACvC,IAAK,cAAc,CAAC,cAAc,KAAK,UAAU,CAAC,IAAI,EAAG,CAAC;oBACzD,OAAO;gBACR,CAAC;gBACD,MAAM,OAAO,GAAG,UAAU,CAAC,SAAS,CAAE,CAAC,CAAE,CAAC;gBAE1C,OAAO,CAAC,MAAM,CAAE;oBACf,IAAI;oBACJ,SAAS,EAAE,wBAAwB;oBACnC,OAAO,EAAE;wBACR;4BACC,SAAS,EAAE,aAAa;4BACxB,GAAG,EAAE,CAAE,KAAyB,EAAG,EAAE;gCACpC,MAAM,YAAY,GAAG,OAAO,CAAC,UAAU,CAAC,OAAO,CAAE,cAAc,CAAE,CAAC;gCAClE,MAAM,OAAO,GAAG,OAAO,CAAC,UAAU,CAAC,OAAO,CAAE,OAAO,CAAE,CAAC;gCACtD,OAAO,KAAK,CAAC,WAAW,CAAE,IAAI,EAAE,GAAG,YAAY,UAAU,OAAO,IAAI,CAAE,CAAC;4BACxE,CAAC;yBACD;qBACD;iBACD,CAAE,CAAC;YACL,CAAC;SACD,CAAC;IACH,CAAC;CACD,CAAC;AAEF,eAAe,MAAM,CAAC"}
@@ -0,0 +1,89 @@
1
+ import {AST_NODE_TYPES, type TSESLint, type TSESTree} from '@typescript-eslint/utils';
2
+ import {isJQueryCall} from './jquery-executing.js';
3
+
4
+ type Messages = 'vulnerableTagStripping' | 'useTextOnly';
5
+ type Context = TSESLint.RuleContext<Messages, []>;
6
+
7
+
8
+ /**
9
+ * Detects a vulnerable tag stripping pattern: .html(value).text()
10
+ *
11
+ * This pattern can lead to XSS vulnerabilities when HTML is inserted and then text is extracted.
12
+ *
13
+ * @link https://docs.wpvip.com/security/javascript-security-recommendations/#h-stripping-tags
14
+ */
15
+ function isTextAfterHtml( node: TSESTree.CallExpression ): TSESTree.MemberExpression | null {
16
+ // Check if this is a .text() call
17
+ if ( AST_NODE_TYPES.MemberExpression !== node.callee.type || ! ( 'name' in node.callee.property ) ) {
18
+ return null;
19
+ }
20
+
21
+ if ( node.callee.property.name !== 'text' ) {
22
+ return null;
23
+ }
24
+
25
+ const parentCall = node.callee.object;
26
+ if ( AST_NODE_TYPES.CallExpression !== parentCall.type ) {
27
+ return null;
28
+ }
29
+
30
+ if ( AST_NODE_TYPES.MemberExpression !== parentCall.callee.type || ! ( 'name' in parentCall.callee.property ) ) {
31
+ return null;
32
+ }
33
+
34
+ if ( parentCall.callee.property.name !== 'html' ) {
35
+ return null;
36
+ }
37
+
38
+ return isJQueryCall( parentCall ) ? parentCall.callee : null;
39
+ }
40
+
41
+
42
+ const plugin: TSESLint.RuleModule<Messages> = {
43
+ defaultOptions: [],
44
+ meta: {
45
+ docs: {
46
+ description: 'Disallow jQuery .html().text() chaining which can lead to XSS through tag stripping',
47
+ },
48
+ hasSuggestions: true,
49
+ messages: {
50
+ vulnerableTagStripping: 'Using .html().text() can lead to XSS vulnerabilities through tag stripping. Use only .text()',
51
+ useTextOnly: 'Remove .html() and move the argument to .text()',
52
+ },
53
+ schema: [],
54
+ type: 'problem',
55
+ },
56
+ create( context: Context ): TSESLint.RuleListener {
57
+ return {
58
+ CallExpression( node: TSESTree.CallExpression ) {
59
+ const htmlProperty = isTextAfterHtml( node );
60
+ if ( null === htmlProperty ) {
61
+ return;
62
+ }
63
+ const jquerySelector = htmlProperty.object;
64
+ const parentCall = htmlProperty.parent;
65
+ if ( AST_NODE_TYPES.CallExpression !== parentCall.type ) {
66
+ return;
67
+ }
68
+ const htmlArg = parentCall.arguments[ 0 ];
69
+
70
+ context.report( {
71
+ node,
72
+ messageId: 'vulnerableTagStripping',
73
+ suggest: [
74
+ {
75
+ messageId: 'useTextOnly',
76
+ fix: ( fixer: TSESLint.RuleFixer ) => {
77
+ const selectorText = context.sourceCode.getText( jquerySelector );
78
+ const argText = context.sourceCode.getText( htmlArg );
79
+ return fixer.replaceText( node, `${selectorText}.text( ${argText} )` );
80
+ },
81
+ },
82
+ ],
83
+ } );
84
+ },
85
+ };
86
+ },
87
+ };
88
+
89
+ export default plugin;
@@ -57,7 +57,7 @@ const plugin = {
57
57
  messages: {
58
58
  unsafeWindow: 'Assignment to "{{propName}}" must be sanitized.',
59
59
  unsafeWindowLocation: 'Assignment to window.location.{{propName}} must be sanitized.',
60
- unsafeRead: 'Data from JS global {{propName}} may contain user-supplied values and should be sanitized before output to prevent XSS.',
60
+ unsafeRead: 'Data from JS global {{propName}} may contain user-supplied values and should be sanitized before using to prevent XSS.',
61
61
  // Suggestions
62
62
  domPurify: 'Wrap the argument with a `DOMPurify.sanitize()` call.',
63
63
  sanitize: 'Wrap the argument with a `sanitize()` call.',
@@ -179,3 +179,4 @@ const plugin = {
179
179
  },
180
180
  };
181
181
  export default plugin;
182
+ //# sourceMappingURL=window-escaping.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"window-escaping.js","sourceRoot":"","sources":["window-escaping.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,220 @@
1
+ import {AST_NODE_TYPES, type TSESLint, type TSESTree} from '@typescript-eslint/utils';
2
+ import {isLiteralString} from '../helpers/string.js';
3
+ import {isSanitized} from '../helpers/dom-purify.js';
4
+
5
+
6
+ type Messages =
7
+ 'unsafeWindow'
8
+ | 'unsafeRead'
9
+ | 'unsafeWindowLocation'
10
+ | 'domPurify'
11
+ | 'sanitize'
12
+
13
+ type Context = TSESLint.RuleContext<Messages, []>;
14
+
15
+
16
+ // Window and location properties that need special handling
17
+ const LOCATION_PROPS = new Set( [ 'href', 'src', 'action',
18
+ 'protocol', 'host', 'hostname', 'pathname', 'search', 'hash', 'username', 'port', 'name', 'status',
19
+ ] );
20
+
21
+ const WINDOW_PROPS = new Set( [ 'name', 'status' ] );
22
+
23
+ export function isSafeUrlString( value: string ): boolean {
24
+ return ! /^\s*(?:javascript|data|vbscript|about|livescript)\s*:/i.test( decodeURIComponent( value.replace( /[\u0000-\u001F\u007F]+/g, '' ) ) );
25
+ }
26
+
27
+
28
+ function isSafeUrlLiteral( node: TSESTree.Expression ): boolean {
29
+ return isLiteralString( node ) && isSafeUrlString( node.value );
30
+ }
31
+
32
+
33
+ function isWindowLocationAssignment( node: TSESTree.AssignmentExpression ): boolean {
34
+ // window.location.<prop> = ...
35
+ return (
36
+ AST_NODE_TYPES.MemberExpression === node.left.type &&
37
+ AST_NODE_TYPES.MemberExpression === node.left.object.type &&
38
+ AST_NODE_TYPES.Identifier === node.left.object.property.type &&
39
+ AST_NODE_TYPES.Identifier === node.left.object.object.type &&
40
+ AST_NODE_TYPES.Identifier === node.left.property.type &&
41
+ 'window' === node.left.object.object.name &&
42
+ 'location' === node.left.object.property.name &&
43
+ LOCATION_PROPS.has( node.left.property.name )
44
+ );
45
+ }
46
+
47
+
48
+ function isWindowAssignment( node: TSESTree.AssignmentExpression ): boolean {
49
+ // window.<prop> = ...
50
+ return (
51
+ AST_NODE_TYPES.MemberExpression === node.left.type &&
52
+ AST_NODE_TYPES.Identifier === node.left.object.type &&
53
+ AST_NODE_TYPES.Identifier === node.left.property.type &&
54
+ 'window' === node.left.object.name &&
55
+ WINDOW_PROPS.has( node.left.property.name )
56
+ );
57
+ }
58
+
59
+
60
+ function isWindowOrLocation( expression: TSESTree.MemberExpression ): boolean {
61
+ // Helper to detect a window.* or window.location.*
62
+ if ( AST_NODE_TYPES.MemberExpression !== expression.type ) {
63
+ return false;
64
+ }
65
+ if ( AST_NODE_TYPES.Identifier === expression.object.type && 'window' === expression.object.name ) {
66
+ return true;
67
+ }
68
+ if ( AST_NODE_TYPES.MemberExpression === expression.object.type ) {
69
+ const memberObject = expression.object;
70
+ const isObjectWindow = AST_NODE_TYPES.Identifier === memberObject.object.type && 'window' === memberObject.object.name;
71
+ const isPropertyLocation = AST_NODE_TYPES.Identifier === memberObject.property.type && 'location' === memberObject.property.name;
72
+
73
+ return isObjectWindow && isPropertyLocation;
74
+ }
75
+
76
+ return false;
77
+ }
78
+
79
+
80
+ const plugin: TSESLint.RuleModule<Messages> = {
81
+ defaultOptions: [],
82
+ meta: {
83
+ type: 'problem',
84
+ docs: {
85
+ description: 'Require proper escaping for the window and location property access',
86
+ },
87
+ messages: {
88
+ unsafeWindow: 'Assignment to "{{propName}}" must be sanitized.',
89
+ unsafeWindowLocation: 'Assignment to window.location.{{propName}} must be sanitized.',
90
+ unsafeRead: 'Data from JS global {{propName}} may contain user-supplied values and should be sanitized before using to prevent XSS.',
91
+
92
+ // Suggestions
93
+ domPurify: 'Wrap the argument with a `DOMPurify.sanitize()` call.',
94
+ sanitize: 'Wrap the argument with a `sanitize()` call.',
95
+ },
96
+ schema: [],
97
+ hasSuggestions: true,
98
+ },
99
+ create( context: Context ): TSESLint.RuleListener {
100
+ return {
101
+ AssignmentExpression( node: TSESTree.AssignmentExpression ): void {
102
+ const right: TSESTree.Expression = node.right;
103
+ if ( AST_NODE_TYPES.MemberExpression !== node.left.type || ! ( 'name' in node.left.property ) ) {
104
+ return;
105
+ }
106
+ const propName = node.left.property.name;
107
+
108
+ // window.location.<prop> = ...
109
+ if ( isWindowLocationAssignment( node ) ) {
110
+ const rhsResolved: TSESTree.Expression = right;
111
+ if ( ! LOCATION_PROPS.has( propName ) ) {
112
+ return;
113
+ }
114
+ if ( isSafeUrlLiteral( rhsResolved ) || isSanitized( rhsResolved ) ) {
115
+ return;
116
+ }
117
+ context.report( {
118
+ node,
119
+ messageId: 'unsafeWindowLocation',
120
+ data: {
121
+ propName,
122
+ },
123
+ suggest: [
124
+ {
125
+ messageId: 'sanitize',
126
+ fix: ( fixer: TSESLint.RuleFixer ) => {
127
+ const argText = context.sourceCode.getText( right );
128
+ return fixer.replaceText( right, `sanitize( ${argText} )` );
129
+ },
130
+ }, {
131
+ messageId: 'domPurify',
132
+ fix: ( fixer: TSESLint.RuleFixer ) => {
133
+ const argText = context.sourceCode.getText( right );
134
+ return fixer.replaceText( right, `DOMPurify.sanitize( ${argText} )` );
135
+ },
136
+ },
137
+ ],
138
+ } );
139
+ return;
140
+ }
141
+
142
+ // window.<prop> = ...
143
+ if ( isWindowAssignment( node ) ) {
144
+ if ( isSanitized( node.right ) ) {
145
+ return;
146
+ }
147
+ context.report( {
148
+ node,
149
+ messageId: 'unsafeWindow',
150
+ data: {
151
+ propName,
152
+ },
153
+ suggest: [
154
+ {
155
+ messageId: 'sanitize',
156
+ fix: ( fixer: TSESLint.RuleFixer ) => {
157
+ const argText = context.sourceCode.getText( right );
158
+ return fixer.replaceText( right, `sanitize( ${argText} )` );
159
+ },
160
+ }, {
161
+ messageId: 'domPurify',
162
+ fix: ( fixer: TSESLint.RuleFixer ) => {
163
+ const argText = context.sourceCode.getText( right );
164
+ return fixer.replaceText( right, `DOMPurify.sanitize( ${argText} )` );
165
+ },
166
+ },
167
+ ],
168
+ } );
169
+ }
170
+ },
171
+
172
+
173
+ // Check for reading from the window.location properties
174
+ MemberExpression( node: TSESTree.MemberExpression ): void {
175
+ const parent = node.parent;
176
+ if ( AST_NODE_TYPES.AssignmentExpression === parent.type && parent.left === node ) {
177
+ return;
178
+ }
179
+
180
+ if ( ! isWindowOrLocation( node ) || ! ( 'name' in node.property ) ) {
181
+ return;
182
+ }
183
+ const propName = node.property.name;
184
+ if ( ! LOCATION_PROPS.has( propName ) ) {
185
+ return;
186
+ }
187
+
188
+ if ( AST_NODE_TYPES.CallExpression === parent.type && isSanitized( parent ) ) {
189
+ return;
190
+ }
191
+
192
+ context.report( {
193
+ node,
194
+ messageId: 'unsafeRead',
195
+ data: {
196
+ propName,
197
+ },
198
+ suggest: [
199
+ {
200
+ messageId: 'sanitize',
201
+ fix: ( fixer: TSESLint.RuleFixer ) => {
202
+ const argText = context.sourceCode.getText( node );
203
+ return fixer.replaceText( node, `sanitize( ${argText} )` );
204
+ },
205
+ },
206
+ {
207
+ messageId: 'domPurify',
208
+ fix: ( fixer: TSESLint.RuleFixer ) => {
209
+ const argText = context.sourceCode.getText( node );
210
+ return fixer.replaceText( node, `DOMPurify.sanitize( ${argText} )` );
211
+ },
212
+ },
213
+ ],
214
+ } );
215
+ },
216
+ };
217
+ },
218
+ };
219
+
220
+ export default plugin;
@@ -0,0 +1,8 @@
1
+ {
2
+ "extends": "../tsconfig.json",
3
+ "include": [
4
+ "./**/*.ts",
5
+ "../helpers/**/*.ts",
6
+ "../index.ts"
7
+ ]
8
+ }
package/types/index.d.ts DELETED
@@ -1,3 +0,0 @@
1
- import type { FlatConfig } from '@typescript-eslint/utils/ts-eslint';
2
- declare const _default: (FlatConfig.Config | import("eslint").Linter.Config<import("eslint").Linter.RulesRecord>)[];
3
- export default _default;
@@ -1,6 +0,0 @@
1
- import { type TSESTree } from '@typescript-eslint/utils';
2
- /**
3
- * Check if a node is a call to a known sanitization function.
4
- * - Currently recognizes `sanitize(...)` and `DOMPurify.sanitize(...)`.
5
- */
6
- export declare function isSanitized(node: TSESTree.Property['value'] | TSESTree.CallExpressionArgument): boolean;
@@ -1,10 +0,0 @@
1
- import type { TSESLint, TSESTree } from '@typescript-eslint/utils';
2
- /**
3
- * Is the type of variable being passed a DOM element?
4
- *
5
- * - DOM elements are of the type `HTML{*}Element`.
6
- * - DOM elements do not require sanitization.
7
- *
8
- * @link https://typescript-eslint.io/developers/custom-rules/#typed-rules
9
- */
10
- export declare function isDomElementType<Context extends Readonly<TSESLint.RuleContext<string, readonly []>>>(expression: TSESTree.CallExpressionArgument, context: Context): boolean;
@@ -1,20 +0,0 @@
1
- import { type TSESLint, type TSESTree } from '@typescript-eslint/utils';
2
- /**
3
- * Is the node of type string.
4
- * - String literals.
5
- * - constants of type string.
6
- * - template literals.
7
- * - intrinsic type string.
8
- */
9
- export declare function isStringLike(node: TSESTree.CallExpressionArgument, context: Readonly<TSESLint.RuleContext<string, readonly []>>): boolean;
10
- /**
11
- * Check if a node is a literal string
12
- */
13
- export declare function isLiteralString(node: TSESTree.Property['value'] | TSESTree.CallExpressionArgument): node is TSESTree.StringLiteral;
14
- /**
15
- * Check if a node is a literal string that is safe to use in an HTML context.
16
- * - Must be a literal string. Or a conditional expression where both branches are safe literal strings.
17
- * - Must not contain `<script`.
18
- * - Must not start with a dangerous protocol (javascript:, data:, vbscript:, about:, livescript:).
19
- */
20
- export declare function isSafeLiteralString(node: TSESTree.Property['value'] | TSESTree.CallExpressionArgument): boolean;
@@ -1,6 +0,0 @@
1
- import { type TSESLint, type TSESTree } from '@typescript-eslint/utils';
2
- import type { Type } from 'typescript';
3
- /**
4
- * Get the TypeScript type of node.
5
- */
6
- export declare function getType<Context extends Readonly<TSESLint.RuleContext<string, readonly []>>>(expression: TSESTree.CallExpressionArgument, context: Context): Type;
@@ -1,8 +0,0 @@
1
- import type { FlatConfig } from '@typescript-eslint/utils/ts-eslint';
2
- type Plugin = FlatConfig.Plugin & {
3
- configs: {
4
- recommended: FlatConfig.Config;
5
- };
6
- };
7
- declare const plugin: Plugin;
8
- export default plugin;
@@ -1,4 +0,0 @@
1
- import { type TSESLint } from '@typescript-eslint/utils';
2
- type Messages = 'dangerousInnerHtml' | 'sanitize' | 'domPurify';
3
- declare const plugin: TSESLint.RuleModule<Messages>;
4
- export default plugin;
@@ -1,4 +0,0 @@
1
- import { type TSESLint } from '@typescript-eslint/utils';
2
- type Messages = 'executed' | 'sanitize' | 'domPurify';
3
- declare const plugin: TSESLint.RuleModule<Messages>;
4
- export default plugin;
@@ -1,6 +0,0 @@
1
- import { type TSESLint } from '@typescript-eslint/utils';
2
- type HtmlExecutingFunctions = 'document.write' | 'document.writeln';
3
- type UnsafeCalls = 'after' | 'append' | 'before' | 'insertAdjacentHTML' | 'prepend' | 'replaceWith' | 'setAttribute';
4
- type Messages = HtmlExecutingFunctions | UnsafeCalls | 'sanitize' | 'domPurify';
5
- declare const plugin: TSESLint.RuleModule<Messages>;
6
- export default plugin;
@@ -1,4 +0,0 @@
1
- import { type TSESLint } from '@typescript-eslint/utils';
2
- type Messages = 'setTimeoutString' | 'setIntervalString' | 'windowOpenUnsanitized' | 'cssTextUnsanitized' | 'sanitize' | 'domPurify';
3
- declare const plugin: TSESLint.RuleModule<Messages>;
4
- export default plugin;
@@ -1,9 +0,0 @@
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;
8
- declare const plugin: TSESLint.RuleModule<'htmlStringConcat'>;
9
- export default plugin;
@@ -1,17 +0,0 @@
1
- import { type TSESLint, type TSESTree } from '@typescript-eslint/utils';
2
- type UnsafeCalls = 'after' | 'append' | 'appendTo' | 'before' | 'html' | 'insertAfter' | 'insertBefore' | 'prepend' | 'prependTo' | 'replaceAll' | 'replaceWith';
3
- type Messages = 'needsEscaping' | 'sanitize' | 'domPurify';
4
- type Context = TSESLint.RuleContext<Messages, []>;
5
- /**
6
- * Is the type of variable being passed a jQuery element?
7
- *
8
- * - jQuery elements are of type `JQuery`.
9
- * - jQuery elements do not require sanitization.
10
- *
11
- * @link https://typescript-eslint.io/developers/custom-rules/#typed-rules
12
- */
13
- export declare function isJQueryElementType(arg: TSESTree.CallExpressionArgument, context: Context): boolean;
14
- export declare function isJQueryCall(node: TSESTree.CallExpression): boolean;
15
- export declare function getJQueryCall(node: TSESTree.CallExpression): UnsafeCalls | null;
16
- declare const plugin: TSESLint.RuleModule<Messages>;
17
- export default plugin;
@@ -1,4 +0,0 @@
1
- import { type TSESLint } from '@typescript-eslint/utils';
2
- type Messages = 'vulnerableTagStripping' | 'useTextOnly';
3
- declare const plugin: TSESLint.RuleModule<Messages>;
4
- export default plugin;
@@ -1,5 +0,0 @@
1
- import { type TSESLint } from '@typescript-eslint/utils';
2
- type Messages = 'unsafeWindow' | 'unsafeRead' | 'unsafeWindowLocation' | 'domPurify' | 'sanitize';
3
- export declare function isSafeUrlString(value: string): boolean;
4
- declare const plugin: TSESLint.RuleModule<Messages>;
5
- export default plugin;