@lipemat/eslint-config 5.0.0 → 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.
- package/helpers/config.js +1 -0
- package/helpers/config.js.map +1 -0
- package/{types/helpers/config.d.ts → helpers/config.ts} +16 -2
- package/index.js +9 -0
- package/package.json +8 -11
- package/plugins/security/helpers/dom-purify.js +1 -0
- package/plugins/security/helpers/dom-purify.js.map +1 -0
- package/plugins/security/helpers/dom-purify.ts +21 -0
- package/plugins/security/helpers/element.js +1 -0
- package/plugins/security/helpers/element.js.map +1 -0
- package/plugins/security/helpers/element.ts +22 -0
- package/plugins/security/helpers/string.js +1 -0
- package/plugins/security/helpers/string.js.map +1 -0
- package/plugins/security/helpers/string.ts +44 -0
- package/plugins/security/helpers/ts-types.js +1 -0
- package/plugins/security/helpers/ts-types.js.map +1 -0
- package/plugins/security/helpers/ts-types.ts +11 -0
- package/plugins/security/index.js +10 -0
- package/plugins/security/index.js.map +1 -0
- package/plugins/security/index.ts +74 -0
- package/plugins/security/package.json +25 -0
- package/plugins/security/rules/dangerously-set-inner-html.js +1 -0
- package/plugins/security/rules/dangerously-set-inner-html.js.map +1 -0
- package/plugins/security/rules/dangerously-set-inner-html.ts +93 -0
- package/plugins/security/rules/html-executing-assignment.js +1 -0
- package/plugins/security/rules/html-executing-assignment.js.map +1 -0
- package/plugins/security/rules/html-executing-assignment.ts +78 -0
- package/plugins/security/rules/html-executing-function.js +1 -0
- package/plugins/security/rules/html-executing-function.js.map +1 -0
- package/plugins/security/rules/html-executing-function.ts +164 -0
- package/plugins/security/rules/html-sinks.js +1 -0
- package/plugins/security/rules/html-sinks.js.map +1 -0
- package/plugins/security/rules/html-sinks.ts +146 -0
- package/plugins/security/rules/html-string-concat.js +1 -0
- package/plugins/security/rules/html-string-concat.js.map +1 -0
- package/plugins/security/rules/html-string-concat.ts +71 -0
- package/plugins/security/rules/jquery-executing.js +1 -0
- package/plugins/security/rules/jquery-executing.js.map +1 -0
- package/plugins/security/rules/jquery-executing.ts +134 -0
- package/plugins/security/rules/no-at-html-tags.js +53 -0
- package/plugins/security/rules/no-at-html-tags.js.map +1 -0
- package/plugins/security/rules/no-at-html-tags.ts +74 -0
- package/plugins/security/rules/vulnerable-tag-stripping.js +1 -0
- package/plugins/security/rules/vulnerable-tag-stripping.js.map +1 -0
- package/plugins/security/rules/vulnerable-tag-stripping.ts +89 -0
- package/plugins/security/rules/window-escaping.js +2 -1
- package/plugins/security/rules/window-escaping.js.map +1 -0
- package/plugins/security/rules/window-escaping.ts +220 -0
- package/plugins/tsconfig.json +8 -0
- package/types/index.d.ts +0 -3
- package/types/plugins/security/helpers/dom-purify.d.ts +0 -6
- package/types/plugins/security/helpers/element.d.ts +0 -10
- package/types/plugins/security/helpers/string.d.ts +0 -20
- package/types/plugins/security/helpers/ts-types.d.ts +0 -6
- package/types/plugins/security/index.d.ts +0 -8
- package/types/plugins/security/rules/dangerously-set-inner-html.d.ts +0 -4
- package/types/plugins/security/rules/html-executing-assignment.d.ts +0 -4
- package/types/plugins/security/rules/html-executing-function.d.ts +0 -6
- package/types/plugins/security/rules/html-sinks.d.ts +0 -4
- package/types/plugins/security/rules/html-string-concat.d.ts +0 -9
- package/types/plugins/security/rules/jquery-executing.d.ts +0 -17
- package/types/plugins/security/rules/vulnerable-tag-stripping.d.ts +0 -4
- package/types/plugins/security/rules/window-escaping.d.ts +0 -5
package/helpers/config.js
CHANGED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"config.js","sourceRoot":"","sources":["config.ts"],"names":[],"mappings":"AAAA,OAAO,EAAC,mBAAmB,EAAC,MAAM,2CAA2C,CAAC;AAM9E;;;;;;;;;;;;;;;;;;;GAmBG;AACH,MAAM,UAAU,SAAS,CAAE,OAA4B;IACtD,MAAM,IAAI,GAAG;QACZ,OAAO;KACP,CAAC;IACF,MAAM,YAAY,GAAqB;QACtC,GAAG,IAAI;QACP,GAAG,mBAAmB,CAAoB,eAAe,EAAE,IAAI,CAAE;KACjE,CAAC;IACF,OAAO,YAAY,CAAC,OAAO,CAAC;AAC7B,CAAC"}
|
|
@@ -1,4 +1,9 @@
|
|
|
1
|
-
import
|
|
1
|
+
import {getExtensionsConfig} from '@lipemat/js-boilerplate/helpers/config.js';
|
|
2
|
+
import type {FlatConfig} from '@typescript-eslint/utils/ts-eslint';
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
export type ExtensionConfigs = { configs: FlatConfig.Config[] };
|
|
6
|
+
|
|
2
7
|
/**
|
|
3
8
|
* Get a config from our /index.js merged with any
|
|
4
9
|
* matching configuration from the project directory.
|
|
@@ -19,4 +24,13 @@ import type { FlatConfig } from '@typescript-eslint/utils/ts-eslint';
|
|
|
19
24
|
* return config
|
|
20
25
|
* }
|
|
21
26
|
*/
|
|
22
|
-
export
|
|
27
|
+
export function getConfig( configs: FlatConfig.Config[] ): FlatConfig.Config[] {
|
|
28
|
+
const BASE = {
|
|
29
|
+
configs,
|
|
30
|
+
};
|
|
31
|
+
const mergedConfig: ExtensionConfigs = {
|
|
32
|
+
...BASE,
|
|
33
|
+
...getExtensionsConfig<ExtensionConfigs>( 'eslint.config', BASE ),
|
|
34
|
+
};
|
|
35
|
+
return mergedConfig.configs;
|
|
36
|
+
}
|
package/index.js
CHANGED
|
@@ -108,12 +108,20 @@ const TS_CONFIG = {
|
|
|
108
108
|
}],
|
|
109
109
|
},
|
|
110
110
|
};
|
|
111
|
+
const TESTS_CONFIG = {
|
|
112
|
+
files: ['**/*.test.ts', '**/*.test.tsx'],
|
|
113
|
+
rules: {
|
|
114
|
+
// Allow test mocking to use `var`.
|
|
115
|
+
'no-var': 'off',
|
|
116
|
+
},
|
|
117
|
+
};
|
|
111
118
|
/**
|
|
112
119
|
* Merge in any extensions' config.
|
|
113
120
|
*/
|
|
114
121
|
const defaultConfig = [
|
|
115
122
|
BASE_CONFIG,
|
|
116
123
|
TS_CONFIG,
|
|
124
|
+
TESTS_CONFIG,
|
|
117
125
|
securityPlugin.configs.recommended,
|
|
118
126
|
];
|
|
119
127
|
let mergedConfig = [];
|
|
@@ -129,3 +137,4 @@ export default [
|
|
|
129
137
|
...fixupConfigRules(flatCompat.extends('plugin:deprecation/recommended')),
|
|
130
138
|
...mergedConfig,
|
|
131
139
|
];
|
|
140
|
+
//# sourceMappingURL=index.js.map
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lipemat/eslint-config",
|
|
3
|
-
"version": "5.0.
|
|
3
|
+
"version": "5.1.0-beta.1",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"description": "Eslint configuration for all @lipemat packages",
|
|
6
6
|
"engines": {
|
|
@@ -8,12 +8,10 @@
|
|
|
8
8
|
},
|
|
9
9
|
"type": "module",
|
|
10
10
|
"main": "index.js",
|
|
11
|
-
"types": "types",
|
|
12
11
|
"files": [
|
|
13
12
|
"index.js",
|
|
14
|
-
"helpers
|
|
15
|
-
"plugins
|
|
16
|
-
"types"
|
|
13
|
+
"helpers",
|
|
14
|
+
"plugins"
|
|
17
15
|
],
|
|
18
16
|
"repository": {
|
|
19
17
|
"type": "git",
|
|
@@ -24,11 +22,11 @@
|
|
|
24
22
|
},
|
|
25
23
|
"homepage": "https://github.com/lipemat/eslint-config#readme",
|
|
26
24
|
"scripts": {
|
|
27
|
-
"build": "tsc --project ./plugins
|
|
25
|
+
"build": "tsc --project ./plugins",
|
|
28
26
|
"prepublishOnly": "yarn run build",
|
|
29
27
|
"test": "lipemat-js-boilerplate test",
|
|
30
28
|
"validate-ts": "tsc --project ./plugins --noEmit",
|
|
31
|
-
"watch": "tsc --project ./plugins --watch
|
|
29
|
+
"watch": "tsc --project ./plugins --watch"
|
|
32
30
|
},
|
|
33
31
|
"dependencies": {
|
|
34
32
|
"@eslint/compat": "^1.2.4",
|
|
@@ -52,18 +50,17 @@
|
|
|
52
50
|
"@types/jquery": "^3.5.16",
|
|
53
51
|
"@types/node": "^20",
|
|
54
52
|
"@typescript-eslint/rule-tester": "^8.40.0",
|
|
55
|
-
"@typescript-eslint/types": "^8.40.0",
|
|
56
53
|
"dompurify": "^3.2.6",
|
|
57
54
|
"execa": "^5.1.1",
|
|
58
55
|
"jest": "^29",
|
|
59
56
|
"jest-runner-eslint": "^2.2.1",
|
|
60
|
-
"typescript": "5.8.3"
|
|
57
|
+
"typescript": "~5.8.3"
|
|
61
58
|
},
|
|
62
59
|
"peerDependencies": {
|
|
63
|
-
"typescript": "
|
|
60
|
+
"typescript": "~5.8.3"
|
|
64
61
|
},
|
|
65
62
|
"resolutions": {
|
|
66
63
|
"@babel/runtime": "^7.27.0"
|
|
67
64
|
},
|
|
68
|
-
"packageManager": "yarn@4.9.
|
|
65
|
+
"packageManager": "yarn@4.9.4"
|
|
69
66
|
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"dom-purify.js","sourceRoot":"","sources":["dom-purify.ts"],"names":[],"mappings":"AAAA,OAAO,EAAC,cAAc,EAAgB,MAAM,0BAA0B,CAAC;AAGvE;;;GAGG;AACH,MAAM,UAAU,WAAW,CAAE,IAAsF;IAClH,IAAK,cAAc,CAAC,cAAc,KAAK,IAAI,CAAC,IAAI,EAAG,CAAC;QACnD,OAAO,KAAK,CAAC;IACd,CAAC;IACD,IAAK,cAAc,CAAC,UAAU,KAAK,IAAI,CAAC,MAAM,CAAC,IAAI,IAAI,UAAU,KAAK,IAAI,CAAC,MAAM,CAAC,IAAI,EAAG,CAAC;QACzF,OAAO,IAAI,CAAC;IACb,CAAC;IAED,IAAK,cAAc,CAAC,gBAAgB,KAAK,IAAI,CAAC,MAAM,CAAC,IAAI,IAAI,cAAc,CAAC,UAAU,KAAK,IAAI,CAAC,MAAM,CAAC,MAAM,CAAC,IAAI,EAAG,CAAC;QACrH,OAAO,WAAW,KAAK,IAAI,CAAC,MAAM,CAAC,MAAM,CAAC,IAAI,CAAC,WAAW,EAAE;YAC3D,cAAc,CAAC,UAAU,KAAK,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,IAAI,IAAI,UAAU,KAAK,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,IAAI,CAAC;IACtG,CAAC;IACD,OAAO,KAAK,CAAC;AACd,CAAC"}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import {AST_NODE_TYPES, type TSESTree} from '@typescript-eslint/utils';
|
|
2
|
+
import type ESTree from 'estree';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Check if a node is a call to a known sanitization function.
|
|
6
|
+
* - Currently recognizes `sanitize(...)` and `DOMPurify.sanitize(...)`.
|
|
7
|
+
*/
|
|
8
|
+
export function isSanitized( node: ESTree.Expression | TSESTree.Property['value'] | TSESTree.CallExpressionArgument ): boolean {
|
|
9
|
+
if ( AST_NODE_TYPES.CallExpression !== node.type ) {
|
|
10
|
+
return false;
|
|
11
|
+
}
|
|
12
|
+
if ( AST_NODE_TYPES.Identifier === node.callee.type && 'sanitize' === node.callee.name ) {
|
|
13
|
+
return true;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
if ( AST_NODE_TYPES.MemberExpression === node.callee.type && AST_NODE_TYPES.Identifier === node.callee.object.type ) {
|
|
17
|
+
return 'dompurify' === node.callee.object.name.toLowerCase() &&
|
|
18
|
+
AST_NODE_TYPES.Identifier === node.callee.property.type && 'sanitize' === node.callee.property.name;
|
|
19
|
+
}
|
|
20
|
+
return false;
|
|
21
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"element.js","sourceRoot":"","sources":["element.ts"],"names":[],"mappings":"AAEA,OAAO,EAAC,OAAO,EAAC,MAAM,eAAe,CAAC;AAEtC;;;;;;;GAOG;AACH,MAAM,UAAU,gBAAgB,CAAuE,UAA2C,EAAE,OAAgB;IACnK,MAAM,IAAI,GAAS,OAAO,CAAW,UAAU,EAAE,OAAO,CAAE,CAAC;IAE3D,MAAM,IAAI,GAAG,IAAI,CAAC,SAAS,EAAE,EAAE,WAAW,IAAI,EAAE,CAAC;IACjD,4FAA4F;IAC5F,IAAK,SAAS,KAAK,IAAI,EAAG,CAAC;QAC1B,OAAO,IAAI,CAAC;IACb,CAAC;IACD,OAAO,IAAI,CAAC,UAAU,CAAE,MAAM,CAAE,IAAI,IAAI,CAAC,QAAQ,CAAE,SAAS,CAAE,CAAC;AAChE,CAAC"}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type {TSESLint, TSESTree} from '@typescript-eslint/utils';
|
|
2
|
+
import type {Type} from 'typescript';
|
|
3
|
+
import {getType} from './ts-types.js';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Is the type of variable being passed a DOM element?
|
|
7
|
+
*
|
|
8
|
+
* - DOM elements are of the type `HTML{*}Element`.
|
|
9
|
+
* - DOM elements do not require sanitization.
|
|
10
|
+
*
|
|
11
|
+
* @link https://typescript-eslint.io/developers/custom-rules/#typed-rules
|
|
12
|
+
*/
|
|
13
|
+
export function isDomElementType<Context extends Readonly<TSESLint.RuleContext<string, readonly []>>>( expression: TSESTree.CallExpressionArgument, context: Context ): boolean {
|
|
14
|
+
const type: Type = getType<Context>( expression, context );
|
|
15
|
+
|
|
16
|
+
const name = type.getSymbol()?.escapedName ?? '';
|
|
17
|
+
// Match any type that ends with "Element", e.g., HTMLElement, HTMLDivElement, Element, etc.
|
|
18
|
+
if ( 'Element' === name ) {
|
|
19
|
+
return true;
|
|
20
|
+
}
|
|
21
|
+
return name.startsWith( 'HTML' ) && name.endsWith( 'Element' );
|
|
22
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"string.js","sourceRoot":"","sources":["string.ts"],"names":[],"mappings":"AAAA,OAAO,EAAC,cAAc,EAA+B,MAAM,0BAA0B,CAAC;AAEtF,OAAO,EAAC,OAAO,EAAC,MAAM,eAAe,CAAC;AAEtC;;;;;;GAMG;AACH,MAAM,UAAU,YAAY,CAAE,IAAqC,EAAE,OAA4D;IAChI,MAAM,IAAI,GAAG,OAAO,CAAE,IAAI,EAAE,OAAO,CAAE,CAAC;IACtC,MAAM,OAAO,GAAG,IAAI,CAAC,eAAe,EAAE,CAAC;IACvC,MAAM,SAAS,GAAG,eAAe,IAAI,IAAI,IAAI,QAAQ,KAAK,IAAI,CAAC,aAAa,CAAC;IAC7E,OAAO,CAAE,cAAc,CAAC,OAAO,KAAK,IAAI,CAAC,IAAI,IAAI,QAAQ,KAAK,OAAO,IAAI,CAAC,KAAK,CAAE,IAAI,CAAE,cAAc,CAAC,eAAe,KAAK,IAAI,CAAC,IAAI,CAAE,IAAI,OAAO,IAAI,SAAS,CAAC;AAC/J,CAAC;AAED;;GAEG;AACH,MAAM,UAAU,eAAe,CAAE,IAAkE;IAClG,OAAO,cAAc,CAAC,OAAO,KAAK,IAAI,CAAC,IAAI,IAAI,QAAQ,KAAK,OAAO,IAAI,CAAC,KAAK,CAAC;AAC/E,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,mBAAmB,CAAE,IAAkE;IACtG,IAAK,cAAc,CAAC,qBAAqB,KAAK,IAAI,CAAC,IAAI,EAAG,CAAC;QAC1D,OAAO,mBAAmB,CAAE,IAAI,CAAC,UAAU,CAAE,IAAI,mBAAmB,CAAE,IAAI,CAAC,SAAS,CAAE,CAAC;IACxF,CAAC;IAED,IAAK,CAAE,eAAe,CAAE,IAAI,CAAE,EAAG,CAAC;QACjC,OAAO,KAAK,CAAC;IACd,CAAC;IACD,IAAK,IAAI,CAAC,KAAK,CAAC,QAAQ,CAAE,SAAS,CAAE,EAAG,CAAC;QACxC,OAAO,KAAK,CAAC;IACd,CAAC;IACD,OAAO,CAAE,wDAAwD,CAAC,IAAI,CAAE,kBAAkB,CAAE,IAAI,CAAC,KAAK,CAAC,OAAO,CAAE,yBAAyB,EAAE,EAAE,CAAE,CAAE,CAAE,CAAC;AACrJ,CAAC"}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import {AST_NODE_TYPES, type TSESLint, type TSESTree} from '@typescript-eslint/utils';
|
|
2
|
+
|
|
3
|
+
import {getType} from './ts-types.js';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Is the node of type string.
|
|
7
|
+
* - String literals.
|
|
8
|
+
* - constants of type string.
|
|
9
|
+
* - template literals.
|
|
10
|
+
* - intrinsic type string.
|
|
11
|
+
*/
|
|
12
|
+
export function isStringLike( node: TSESTree.CallExpressionArgument, context: Readonly<TSESLint.RuleContext<string, readonly []>> ): boolean {
|
|
13
|
+
const type = getType( node, context );
|
|
14
|
+
const literal = type.isStringLiteral();
|
|
15
|
+
const intrinsic = 'intrinsicName' in type && 'string' === type.intrinsicName;
|
|
16
|
+
return ( AST_NODE_TYPES.Literal === node.type && 'string' === typeof node.value ) || ( AST_NODE_TYPES.TemplateLiteral === node.type ) || literal || intrinsic;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Check if a node is a literal string
|
|
21
|
+
*/
|
|
22
|
+
export function isLiteralString( node: TSESTree.Property['value'] | TSESTree.CallExpressionArgument ): node is TSESTree.StringLiteral {
|
|
23
|
+
return AST_NODE_TYPES.Literal === node.type && 'string' === typeof node.value;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Check if a node is a literal string that is safe to use in an HTML context.
|
|
28
|
+
* - Must be a literal string. Or a conditional expression where both branches are safe literal strings.
|
|
29
|
+
* - Must not contain `<script`.
|
|
30
|
+
* - Must not start with a dangerous protocol (javascript:, data:, vbscript:, about:, livescript:).
|
|
31
|
+
*/
|
|
32
|
+
export function isSafeLiteralString( node: TSESTree.Property['value'] | TSESTree.CallExpressionArgument ): boolean {
|
|
33
|
+
if ( AST_NODE_TYPES.ConditionalExpression === node.type ) {
|
|
34
|
+
return isSafeLiteralString( node.consequent ) && isSafeLiteralString( node.alternate );
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
if ( ! isLiteralString( node ) ) {
|
|
38
|
+
return false;
|
|
39
|
+
}
|
|
40
|
+
if ( node.value.includes( '<script' ) ) {
|
|
41
|
+
return false;
|
|
42
|
+
}
|
|
43
|
+
return ! /^\s*(?:javascript|data|vbscript|about|livescript)\s*:/i.test( decodeURIComponent( node.value.replace( /[\u0000-\u001F\u007F]+/g, '' ) ) );
|
|
44
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ts-types.js","sourceRoot":"","sources":["ts-types.ts"],"names":[],"mappings":"AAAA,OAAO,EAAC,WAAW,EAA+B,MAAM,0BAA0B,CAAC;AAGnF;;GAEG;AACH,MAAM,UAAU,OAAO,CAAuE,UAA2C,EAAE,OAAgB;IAC1J,MAAM,EAAC,iBAAiB,EAAC,GAAG,WAAW,CAAC,iBAAiB,CAAE,OAAO,CAAE,CAAC;IACrE,MAAM,IAAI,GAAG,iBAAiB,CAAE,UAAU,CAAE,CAAC;IAC7C,OAAO,IAAI,CAAC,kBAAkB,EAAE,CAAC;AAClC,CAAC"}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import {ESLintUtils, type TSESLint, type TSESTree} from '@typescript-eslint/utils';
|
|
2
|
+
import type {Type} from 'typescript';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Get the TypeScript type of node.
|
|
6
|
+
*/
|
|
7
|
+
export function getType<Context extends Readonly<TSESLint.RuleContext<string, readonly []>>>( expression: TSESTree.CallExpressionArgument, context: Context ): Type {
|
|
8
|
+
const {getTypeAtLocation} = ESLintUtils.getParserServices( context );
|
|
9
|
+
const type = getTypeAtLocation( expression );
|
|
10
|
+
return type.getNonNullableType();
|
|
11
|
+
}
|
|
@@ -6,6 +6,7 @@ import htmlStringConcat from './rules/html-string-concat.js';
|
|
|
6
6
|
import jqueryExecuting from './rules/jquery-executing.js';
|
|
7
7
|
import vulnerableTagStripping from './rules/vulnerable-tag-stripping.js';
|
|
8
8
|
import windowEscaping from './rules/window-escaping.js';
|
|
9
|
+
import noAtHtmlTags from './rules/no-at-html-tags.js';
|
|
9
10
|
import { readFileSync } from 'fs';
|
|
10
11
|
import { resolve } from 'path';
|
|
11
12
|
const pkg = JSON.parse(readFileSync(resolve('./package.json'), 'utf8'));
|
|
@@ -21,11 +22,13 @@ const plugin = {
|
|
|
21
22
|
'html-sinks': htmlSinks,
|
|
22
23
|
'html-string-concat': htmlStringConcat,
|
|
23
24
|
'jquery-executing': jqueryExecuting,
|
|
25
|
+
'no-at-html-tags': noAtHtmlTags,
|
|
24
26
|
'vulnerable-tag-stripping': vulnerableTagStripping,
|
|
25
27
|
'window-escaping': windowEscaping,
|
|
26
28
|
},
|
|
27
29
|
configs: {
|
|
28
30
|
recommended: {},
|
|
31
|
+
svelte: {},
|
|
29
32
|
},
|
|
30
33
|
};
|
|
31
34
|
// Freeze the plugin to prevent modifications and use the plugin within.
|
|
@@ -45,5 +48,12 @@ plugin.configs = Object.freeze({
|
|
|
45
48
|
'@lipemat/security/window-escaping': 'error',
|
|
46
49
|
},
|
|
47
50
|
},
|
|
51
|
+
svelte: {
|
|
52
|
+
files: ['**/*.svelte', '*.svelte'],
|
|
53
|
+
rules: {
|
|
54
|
+
'@lipemat/security/no-at-html-tags': 'error',
|
|
55
|
+
},
|
|
56
|
+
},
|
|
48
57
|
});
|
|
49
58
|
export default plugin;
|
|
59
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["index.ts"],"names":[],"mappings":"AAAA,OAAO,uBAAuB,MAAM,uCAAuC,CAAC;AAC5E,OAAO,uBAAuB,MAAM,sCAAsC,CAAC;AAC3E,OAAO,qBAAqB,MAAM,oCAAoC,CAAC;AACvE,OAAO,SAAS,MAAM,uBAAuB,CAAC;AAC9C,OAAO,gBAAgB,MAAM,+BAA+B,CAAC;AAC7D,OAAO,eAAe,MAAM,6BAA6B,CAAC;AAC1D,OAAO,sBAAsB,MAAM,qCAAqC,CAAC;AACzE,OAAO,cAAc,MAAM,4BAA4B,CAAC;AACxD,OAAO,YAAY,MAAM,4BAA4B,CAAC;AACtD,OAAO,EAAC,YAAY,EAAC,MAAM,IAAI,CAAC;AAChC,OAAO,EAAC,OAAO,EAAC,MAAM,MAAM,CAAC;AAI7B,MAAM,GAAG,GAAG,IAAI,CAAC,KAAK,CACrB,YAAY,CAAE,OAAO,CAAE,gBAAgB,CAAE,EAAE,MAAM,CAAE,CACnD,CAAC;AAUF,MAAM,MAAM,GAAW;IACtB,IAAI,EAAE;QACL,IAAI,EAAE,GAAG,CAAC,IAAI;QACd,OAAO,EAAE,GAAG,CAAC,OAAO;KACpB;IACD,KAAK,EAAE;QACN,4BAA4B,EAAE,uBAAuB;QACrD,2BAA2B,EAAE,uBAAuB;QACpD,yBAAyB,EAAE,qBAAqB;QAChD,YAAY,EAAE,SAAS;QACvB,oBAAoB,EAAE,gBAAgB;QACtC,kBAAkB,EAAE,eAAe;QACnC,iBAAiB,EAAE,YAAY;QAC/B,0BAA0B,EAAE,sBAAsB;QAClD,iBAAiB,EAAE,cAAc;KACjC;IACD,OAAO,EAAE;QACR,WAAW,EAAE,EAAE;QACf,MAAM,EAAE,EAAE;KACV;CACD,CAAC;AAEF,wEAAwE;AACxE,MAAM,CAAC,OAAO,GAAG,MAAM,CAAC,MAAM,CAAE;IAC/B,WAAW,EAAE;QACZ,OAAO,EAAE;YACR,mBAAmB,EAAE,MAAM;SAC3B;QACD,KAAK,EAAE;YACN,8CAA8C,EAAE,OAAO;YACvD,6CAA6C,EAAE,OAAO;YACtD,2CAA2C,EAAE,OAAO;YACpD,8BAA8B,EAAE,OAAO;YACvC,sCAAsC,EAAE,OAAO;YAC/C,oCAAoC,EAAE,OAAO;YAC7C,4CAA4C,EAAE,OAAO;YACrD,mCAAmC,EAAE,OAAO;SAC5C;KACD;IACD,MAAM,EAAE;QACP,KAAK,EAAE,CAAE,aAAa,EAAE,UAAU,CAAE;QACpC,KAAK,EAAE;YACN,mCAAmC,EAAE,OAAO;SAC5C;KACD;CACD,CAAE,CAAC;AAEJ,eAAe,MAAM,CAAC"}
|
|
@@ -0,0 +1,74 @@
|
|
|
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 noAtHtmlTags from './rules/no-at-html-tags.js';
|
|
10
|
+
import {readFileSync} from 'fs';
|
|
11
|
+
import {resolve} from 'path';
|
|
12
|
+
import type {FlatConfig} from '@typescript-eslint/utils/ts-eslint';
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
const pkg = JSON.parse(
|
|
16
|
+
readFileSync( resolve( './package.json' ), 'utf8' ),
|
|
17
|
+
);
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
type Plugin = FlatConfig.Plugin & {
|
|
21
|
+
configs: {
|
|
22
|
+
recommended: FlatConfig.Config;
|
|
23
|
+
svelte: FlatConfig.Config;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const plugin: Plugin = {
|
|
28
|
+
meta: {
|
|
29
|
+
name: pkg.name,
|
|
30
|
+
version: pkg.version,
|
|
31
|
+
},
|
|
32
|
+
rules: {
|
|
33
|
+
'dangerously-set-inner-html': dangerouslySetInnerHtml,
|
|
34
|
+
'html-executing-assignment': htmlExecutingAssignment,
|
|
35
|
+
'html-executing-function': htmlExecutingFunction,
|
|
36
|
+
'html-sinks': htmlSinks,
|
|
37
|
+
'html-string-concat': htmlStringConcat,
|
|
38
|
+
'jquery-executing': jqueryExecuting,
|
|
39
|
+
'no-at-html-tags': noAtHtmlTags,
|
|
40
|
+
'vulnerable-tag-stripping': vulnerableTagStripping,
|
|
41
|
+
'window-escaping': windowEscaping,
|
|
42
|
+
},
|
|
43
|
+
configs: {
|
|
44
|
+
recommended: {},
|
|
45
|
+
svelte: {},
|
|
46
|
+
},
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
// Freeze the plugin to prevent modifications and use the plugin within.
|
|
50
|
+
plugin.configs = Object.freeze( {
|
|
51
|
+
recommended: {
|
|
52
|
+
plugins: {
|
|
53
|
+
'@lipemat/security': plugin,
|
|
54
|
+
},
|
|
55
|
+
rules: {
|
|
56
|
+
'@lipemat/security/dangerously-set-inner-html': 'error',
|
|
57
|
+
'@lipemat/security/html-executing-assignment': 'error',
|
|
58
|
+
'@lipemat/security/html-executing-function': 'error',
|
|
59
|
+
'@lipemat/security/html-sinks': 'error',
|
|
60
|
+
'@lipemat/security/html-string-concat': 'error',
|
|
61
|
+
'@lipemat/security/jquery-executing': 'error',
|
|
62
|
+
'@lipemat/security/vulnerable-tag-stripping': 'error',
|
|
63
|
+
'@lipemat/security/window-escaping': 'error',
|
|
64
|
+
},
|
|
65
|
+
},
|
|
66
|
+
svelte: {
|
|
67
|
+
files: [ '**/*.svelte', '*.svelte' ],
|
|
68
|
+
rules: {
|
|
69
|
+
'@lipemat/security/no-at-html-tags': 'error',
|
|
70
|
+
},
|
|
71
|
+
},
|
|
72
|
+
} );
|
|
73
|
+
|
|
74
|
+
export default plugin;
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@lipemat/eslint-plugin-security",
|
|
3
|
+
"description": "ESLint rules to replace PHPCS security scanning",
|
|
4
|
+
"version": "1.0.0",
|
|
5
|
+
"private": true,
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"type": "module",
|
|
8
|
+
"main": "index.js",
|
|
9
|
+
"source": "index.ts",
|
|
10
|
+
"files": [
|
|
11
|
+
"index.ts",
|
|
12
|
+
"index.js",
|
|
13
|
+
"rules/",
|
|
14
|
+
"helpers/"
|
|
15
|
+
],
|
|
16
|
+
"dependencies": {
|
|
17
|
+
"eslint": "^9",
|
|
18
|
+
"@typescript-eslint/utils": "^8.40.0"
|
|
19
|
+
},
|
|
20
|
+
"peerDependencies": {
|
|
21
|
+
"eslint": "^9",
|
|
22
|
+
"@typescript-eslint/parser": "^8.40.0",
|
|
23
|
+
"typescript": "~5.8.3"
|
|
24
|
+
}
|
|
25
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"dangerously-set-inner-html.js","sourceRoot":"","sources":["dangerously-set-inner-html.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import {AST_NODE_TYPES, type TSESLint, type TSESTree} from '@typescript-eslint/utils';
|
|
2
|
+
|
|
3
|
+
import {isSanitized} from '../helpers/dom-purify.js';
|
|
4
|
+
|
|
5
|
+
type Messages = 'dangerousInnerHtml' | 'sanitize' | 'domPurify';
|
|
6
|
+
type Context = TSESLint.RuleContext<Messages, []>;
|
|
7
|
+
|
|
8
|
+
function isDangerouslySetInnerHTML( node: TSESTree.JSXAttribute ): boolean {
|
|
9
|
+
return (
|
|
10
|
+
'JSXAttribute' === node.type &&
|
|
11
|
+
'dangerouslySetInnerHTML' === node.name.name
|
|
12
|
+
);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function getDangerouslySetInnerHTMLValue( node: TSESTree.JSXAttribute ): null | TSESTree.Property['value'] {
|
|
16
|
+
// Expecting value like: {{ __html: expr }}
|
|
17
|
+
const val = node.value;
|
|
18
|
+
if ( null === val ) {
|
|
19
|
+
return null; // No value provided
|
|
20
|
+
}
|
|
21
|
+
if ( AST_NODE_TYPES.JSXExpressionContainer !== val.type ) {
|
|
22
|
+
return null;
|
|
23
|
+
}
|
|
24
|
+
const expr = val.expression;
|
|
25
|
+
if ( AST_NODE_TYPES.ObjectExpression !== expr.type ) {
|
|
26
|
+
return null;
|
|
27
|
+
}
|
|
28
|
+
const htmlProp = expr.properties.find( p => (
|
|
29
|
+
AST_NODE_TYPES.Property === p.type &&
|
|
30
|
+
(
|
|
31
|
+
( AST_NODE_TYPES.Identifier === p.key.type && '__html' === p.key.name ) ||
|
|
32
|
+
( AST_NODE_TYPES.Literal === p.key.type && '__html' === p.key.value )
|
|
33
|
+
)
|
|
34
|
+
) );
|
|
35
|
+
if ( undefined !== htmlProp && 'value' in htmlProp ) {
|
|
36
|
+
return htmlProp.value;
|
|
37
|
+
}
|
|
38
|
+
return null;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
const plugin: TSESLint.RuleModule<Messages> = {
|
|
43
|
+
defaultOptions: [],
|
|
44
|
+
meta: {
|
|
45
|
+
type: 'problem',
|
|
46
|
+
hasSuggestions: true,
|
|
47
|
+
docs: {
|
|
48
|
+
description: 'Disallow using unsanitized values in dangerouslySetInnerHTML',
|
|
49
|
+
},
|
|
50
|
+
messages: {
|
|
51
|
+
dangerousInnerHtml: 'Any HTML passed to `dangerouslySetInnerHTML` gets executed. Please make sure it\'s properly escaped.',
|
|
52
|
+
|
|
53
|
+
// Suggestions
|
|
54
|
+
domPurify: 'Wrap the content with a `DOMPurify.sanitize()` call.',
|
|
55
|
+
sanitize: 'Wrap the content with a `sanitize()` call.',
|
|
56
|
+
},
|
|
57
|
+
schema: [],
|
|
58
|
+
},
|
|
59
|
+
create( context: Context ): TSESLint.RuleListener {
|
|
60
|
+
return {
|
|
61
|
+
JSXAttribute( node: TSESTree.JSXAttribute ) {
|
|
62
|
+
if ( ! isDangerouslySetInnerHTML( node ) ) {
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
const htmlValue = getDangerouslySetInnerHTMLValue( node );
|
|
66
|
+
if ( null === htmlValue || isSanitized( htmlValue ) ) {
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
context.report( {
|
|
71
|
+
node,
|
|
72
|
+
messageId: 'dangerousInnerHtml',
|
|
73
|
+
suggest: [
|
|
74
|
+
{
|
|
75
|
+
messageId: 'domPurify',
|
|
76
|
+
fix: ( fixer: TSESLint.RuleFixer ) => {
|
|
77
|
+
return fixer.replaceText( node, `dangerouslySetInnerHTML={{__html: DOMPurify.sanitize( ${context.sourceCode.getText( htmlValue )} )}}` );
|
|
78
|
+
},
|
|
79
|
+
},
|
|
80
|
+
{
|
|
81
|
+
messageId: 'sanitize',
|
|
82
|
+
fix: ( fixer: TSESLint.RuleFixer ) => {
|
|
83
|
+
return fixer.replaceText( node, `dangerouslySetInnerHTML={{__html: sanitize( ${context.sourceCode.getText( htmlValue )} )}}` );
|
|
84
|
+
},
|
|
85
|
+
},
|
|
86
|
+
],
|
|
87
|
+
} );
|
|
88
|
+
},
|
|
89
|
+
};
|
|
90
|
+
},
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
export default plugin;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"html-executing-assignment.js","sourceRoot":"","sources":["html-executing-assignment.ts"],"names":[],"mappings":"AAAA,OAAO,EAAC,cAAc,EAA+B,MAAM,0BAA0B,CAAC;AACtF,OAAO,EAAC,mBAAmB,EAAC,MAAM,sBAAsB,CAAC;AACzD,OAAO,EAAC,WAAW,EAAC,MAAM,0BAA0B,CAAC;AACrD,OAAO,EAAC,gBAAgB,EAAC,MAAM,uBAAuB,CAAC;AAOvD,MAAM,iBAAiB,GAAuB;IAC7C,WAAW;IACX,WAAW;CACX,CAAC;AAEF,SAAS,gBAAgB,CAAE,YAAoB;IAC9C,OAAO,iBAAiB,CAAC,QAAQ,CAAE,YAAgC,CAAE,CAAC;AACvE,CAAC;AAED,MAAM,MAAM,GAAkC;IAC7C,cAAc,EAAE,EAAE;IAClB,IAAI,EAAE;QACL,IAAI,EAAE;YACL,WAAW,EAAE,0EAA0E;SACvF;QACD,cAAc,EAAE,IAAI;QACpB,QAAQ,EAAE;YACT,QAAQ,EAAE,wFAAwF;YAElG,cAAc;YACd,SAAS,EAAE,uDAAuD;YAClE,QAAQ,EAAE,6CAA6C;SACvD;QACD,MAAM,EAAE,EAAE;QACV,IAAI,EAAE,SAAS;KACf;IACD,MAAM,CAAE,OAAgB;QACvB,OAAO;YACN,oBAAoB,CAAE,IAAmC;gBACxD,IAAK,cAAc,CAAC,gBAAgB,KAAK,IAAI,CAAC,IAAI,CAAC,IAAI,IAAI,cAAc,CAAC,UAAU,KAAK,IAAI,CAAC,IAAI,CAAC,QAAQ,CAAC,IAAI,EAAG,CAAC;oBACnH,OAAO;gBACR,CAAC;gBACD,MAAM,YAAY,GAAG,IAAI,CAAC,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC;gBAC7C,IAAK,CAAE,gBAAgB,CAAE,YAAY,CAAE,EAAG,CAAC;oBAC1C,OAAO;gBACR,CAAC;gBACD,MAAM,KAAK,GAAG,IAAI,CAAC,KAAK,CAAC;gBACzB,IAAK,CAAE,mBAAmB,CAAE,KAAK,CAAE,IAAI,CAAE,WAAW,CAAE,KAAK,CAAE,IAAI,gBAAgB,CAAW,IAAI,CAAC,IAAI,CAAC,MAAM,EAAE,OAAO,CAAE,EAAG,CAAC;oBAC1H,OAAO,CAAC,MAAM,CAAE;wBACf,IAAI;wBACJ,SAAS,EAAE,UAAU;wBACrB,IAAI,EAAE;4BACL,YAAY;yBACZ;wBACD,OAAO,EAAE;4BACR;gCACC,SAAS,EAAE,WAAW;gCACtB,GAAG,EAAE,CAAE,KAAyB,EAAG,EAAE;oCACpC,MAAM,SAAS,GAAG,OAAO,CAAC,UAAU,CAAC,OAAO,CAAE,KAAK,CAAE,CAAC;oCACtD,OAAO,KAAK,CAAC,WAAW,CAAE,KAAK,EAAE,uBAAuB,SAAS,IAAI,CAAE,CAAC;gCACzE,CAAC;6BACD;4BACD;gCACC,SAAS,EAAE,UAAU;gCACrB,GAAG,EAAE,CAAE,KAAyB,EAAG,EAAE;oCACpC,MAAM,SAAS,GAAG,OAAO,CAAC,UAAU,CAAC,OAAO,CAAE,KAAK,CAAE,CAAC;oCACtD,OAAO,KAAK,CAAC,WAAW,CAAE,KAAK,EAAE,aAAa,SAAS,IAAI,CAAE,CAAC;gCAC/D,CAAC;6BACD;yBACD;qBACD,CAAE,CAAC;gBACL,CAAC;YACF,CAAC;SACD,CAAC;IACH,CAAC;CACD,CAAC;AAEF,eAAe,MAAM,CAAC"}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import {AST_NODE_TYPES, type TSESLint, type TSESTree} from '@typescript-eslint/utils';
|
|
2
|
+
import {isSafeLiteralString} from '../helpers/string.js';
|
|
3
|
+
import {isSanitized} from '../helpers/dom-purify.js';
|
|
4
|
+
import {isDomElementType} from '../helpers/element.js';
|
|
5
|
+
|
|
6
|
+
type UnsafeProperties = 'innerHTML' | 'outerHTML';
|
|
7
|
+
|
|
8
|
+
type Messages = 'executed' | 'sanitize' | 'domPurify';
|
|
9
|
+
type Context = TSESLint.RuleContext<Messages, []>;
|
|
10
|
+
|
|
11
|
+
const UNSAFE_PROPERTIES: UnsafeProperties[] = [
|
|
12
|
+
'innerHTML',
|
|
13
|
+
'outerHTML',
|
|
14
|
+
];
|
|
15
|
+
|
|
16
|
+
function isUnsafeProperty( propertyName: string ): propertyName is UnsafeProperties {
|
|
17
|
+
return UNSAFE_PROPERTIES.includes( propertyName as UnsafeProperties );
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const plugin: TSESLint.RuleModule<Messages> = {
|
|
21
|
+
defaultOptions: [],
|
|
22
|
+
meta: {
|
|
23
|
+
docs: {
|
|
24
|
+
description: 'Disallow using unsanitized values in HTML executing property assignments',
|
|
25
|
+
},
|
|
26
|
+
hasSuggestions: true,
|
|
27
|
+
messages: {
|
|
28
|
+
executed: 'Any HTML used with `{{propertyName}}` gets executed. Make sure it\'s properly escaped.',
|
|
29
|
+
|
|
30
|
+
// Suggestions
|
|
31
|
+
domPurify: 'Wrap the argument with a `DOMPurify.sanitize()` call.',
|
|
32
|
+
sanitize: 'Wrap the argument with a `sanitize()` call.',
|
|
33
|
+
},
|
|
34
|
+
schema: [],
|
|
35
|
+
type: 'problem',
|
|
36
|
+
},
|
|
37
|
+
create( context: Context ): TSESLint.RuleListener {
|
|
38
|
+
return {
|
|
39
|
+
AssignmentExpression( node: TSESTree.AssignmentExpression ) {
|
|
40
|
+
if ( AST_NODE_TYPES.MemberExpression !== node.left.type || AST_NODE_TYPES.Identifier !== node.left.property.type ) {
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
const propertyName = node.left.property.name;
|
|
44
|
+
if ( ! isUnsafeProperty( propertyName ) ) {
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
const value = node.right;
|
|
48
|
+
if ( ! isSafeLiteralString( value ) && ! isSanitized( value ) && isDomElementType<Context>( node.left.object, context ) ) {
|
|
49
|
+
context.report( {
|
|
50
|
+
node,
|
|
51
|
+
messageId: 'executed',
|
|
52
|
+
data: {
|
|
53
|
+
propertyName,
|
|
54
|
+
},
|
|
55
|
+
suggest: [
|
|
56
|
+
{
|
|
57
|
+
messageId: 'domPurify',
|
|
58
|
+
fix: ( fixer: TSESLint.RuleFixer ) => {
|
|
59
|
+
const valueText = context.sourceCode.getText( value );
|
|
60
|
+
return fixer.replaceText( value, `DOMPurify.sanitize( ${valueText} )` );
|
|
61
|
+
},
|
|
62
|
+
},
|
|
63
|
+
{
|
|
64
|
+
messageId: 'sanitize',
|
|
65
|
+
fix: ( fixer: TSESLint.RuleFixer ) => {
|
|
66
|
+
const valueText = context.sourceCode.getText( value );
|
|
67
|
+
return fixer.replaceText( value, `sanitize( ${valueText} )` );
|
|
68
|
+
},
|
|
69
|
+
},
|
|
70
|
+
],
|
|
71
|
+
} );
|
|
72
|
+
}
|
|
73
|
+
},
|
|
74
|
+
};
|
|
75
|
+
},
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
export default plugin;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"html-executing-function.js","sourceRoot":"","sources":["html-executing-function.ts"],"names":[],"mappings":"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"}
|