@stll/oxlint-plugin 0.2.2 → 0.4.0
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/dist/index.cjs +26 -0
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +14 -1
- package/dist/index.d.mts +14 -1
- package/dist/index.mjs +20 -1
- package/dist/index.mjs.map +1 -1
- package/dist/no-any-casts.cjs.map +1 -1
- package/dist/no-checkout-writes-in-tests.cjs +115 -0
- package/dist/no-checkout-writes-in-tests.cjs.map +1 -0
- package/dist/no-checkout-writes-in-tests.d.cts +4 -0
- package/dist/no-checkout-writes-in-tests.d.mts +5 -0
- package/dist/no-checkout-writes-in-tests.mjs +111 -0
- package/dist/no-checkout-writes-in-tests.mjs.map +1 -0
- package/dist/no-dangerous-type-assertions.cjs.map +1 -1
- package/dist/no-partial-record-satisfies.cjs.map +1 -1
- package/dist/no-partial-record-satisfies.mjs +1 -1
- package/dist/no-raw-route-query-client.cjs.map +1 -1
- package/dist/no-raw-route-query-client.mjs +1 -1
- package/dist/no-real-network-in-tests.cjs +71 -0
- package/dist/no-real-network-in-tests.cjs.map +1 -0
- package/dist/no-real-network-in-tests.d.cts +4 -0
- package/dist/no-real-network-in-tests.d.mts +5 -0
- package/dist/no-real-network-in-tests.mjs +67 -0
- package/dist/no-real-network-in-tests.mjs.map +1 -0
- package/dist/no-real-timer-waits-in-tests.cjs +32 -0
- package/dist/no-real-timer-waits-in-tests.cjs.map +1 -0
- package/dist/no-real-timer-waits-in-tests.d.cts +4 -0
- package/dist/no-real-timer-waits-in-tests.d.mts +5 -0
- package/dist/no-real-timer-waits-in-tests.mjs +28 -0
- package/dist/no-real-timer-waits-in-tests.mjs.map +1 -0
- package/dist/no-unsafe-inner-html.cjs +458 -66
- package/dist/no-unsafe-inner-html.cjs.map +1 -1
- package/dist/no-unsafe-inner-html.d.cts +106 -1
- package/dist/no-unsafe-inner-html.d.mts +105 -1
- package/dist/no-unsafe-inner-html.mjs +458 -68
- package/dist/no-unsafe-inner-html.mjs.map +1 -1
- package/dist/no-unseeded-random-in-tests.cjs +25 -0
- package/dist/no-unseeded-random-in-tests.cjs.map +1 -0
- package/dist/no-unseeded-random-in-tests.d.cts +4 -0
- package/dist/no-unseeded-random-in-tests.d.mts +5 -0
- package/dist/no-unseeded-random-in-tests.mjs +21 -0
- package/dist/no-unseeded-random-in-tests.mjs.map +1 -0
- package/dist/no-wall-clock-in-tests.cjs +35 -0
- package/dist/no-wall-clock-in-tests.cjs.map +1 -0
- package/dist/no-wall-clock-in-tests.d.cts +4 -0
- package/dist/no-wall-clock-in-tests.d.mts +5 -0
- package/dist/no-wall-clock-in-tests.mjs +31 -0
- package/dist/no-wall-clock-in-tests.mjs.map +1 -0
- package/dist/require-loader-prefetch.cjs +8 -7
- package/dist/require-loader-prefetch.cjs.map +1 -1
- package/dist/require-loader-prefetch.mjs +8 -7
- package/dist/require-loader-prefetch.mjs.map +1 -1
- package/dist/test-rule-utils.cjs +143 -0
- package/dist/test-rule-utils.cjs.map +1 -0
- package/dist/test-rule-utils.mjs +114 -0
- package/dist/test-rule-utils.mjs.map +1 -0
- package/dist/utils.cjs +85 -10
- package/dist/utils.cjs.map +1 -1
- package/dist/utils.mjs +54 -9
- package/dist/utils.mjs.map +1 -1
- package/package.json +57 -2
|
@@ -1,119 +1,509 @@
|
|
|
1
|
-
import { i as
|
|
1
|
+
import { a as isAstNode, c as memberPropertyName, d as unwrapExpression, i as invokedCallee, l as resolveLiteralArraySpreads, o as isIdentifier, r as getPropertyName, s as isStringLiteral, u as staticStringValue } from "./utils.mjs";
|
|
2
2
|
import { eslintCompatPlugin } from "@oxlint/plugins";
|
|
3
3
|
//#region src/no-unsafe-inner-html.ts
|
|
4
|
+
const DOM_ELEMENT_PRODUCERS = [
|
|
5
|
+
{
|
|
6
|
+
name: "body",
|
|
7
|
+
kind: "property",
|
|
8
|
+
receiver: "document"
|
|
9
|
+
},
|
|
10
|
+
{
|
|
11
|
+
name: "head",
|
|
12
|
+
kind: "property",
|
|
13
|
+
receiver: "document"
|
|
14
|
+
},
|
|
15
|
+
{
|
|
16
|
+
name: "documentElement",
|
|
17
|
+
kind: "property",
|
|
18
|
+
receiver: "document"
|
|
19
|
+
},
|
|
20
|
+
{
|
|
21
|
+
name: "activeElement",
|
|
22
|
+
kind: "property",
|
|
23
|
+
receiver: "document"
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
name: "scrollingElement",
|
|
27
|
+
kind: "property",
|
|
28
|
+
receiver: "document"
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
name: "fullscreenElement",
|
|
32
|
+
kind: "property",
|
|
33
|
+
receiver: "document"
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
name: "createElement",
|
|
37
|
+
kind: "method",
|
|
38
|
+
receiver: "document"
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
name: "createElementNS",
|
|
42
|
+
kind: "method",
|
|
43
|
+
receiver: "document"
|
|
44
|
+
},
|
|
45
|
+
{
|
|
46
|
+
name: "getElementById",
|
|
47
|
+
kind: "method",
|
|
48
|
+
receiver: "document"
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
name: "querySelector",
|
|
52
|
+
kind: "method",
|
|
53
|
+
receiver: "either"
|
|
54
|
+
},
|
|
55
|
+
{
|
|
56
|
+
name: "closest",
|
|
57
|
+
kind: "method",
|
|
58
|
+
receiver: "element"
|
|
59
|
+
},
|
|
60
|
+
{
|
|
61
|
+
name: "elementFromPoint",
|
|
62
|
+
kind: "method",
|
|
63
|
+
receiver: "document"
|
|
64
|
+
},
|
|
65
|
+
{
|
|
66
|
+
name: "getElementsByClassName",
|
|
67
|
+
kind: "collection",
|
|
68
|
+
receiver: "either"
|
|
69
|
+
},
|
|
70
|
+
{
|
|
71
|
+
name: "getElementsByTagName",
|
|
72
|
+
kind: "collection",
|
|
73
|
+
receiver: "either"
|
|
74
|
+
},
|
|
75
|
+
{
|
|
76
|
+
name: "getElementsByTagNameNS",
|
|
77
|
+
kind: "collection",
|
|
78
|
+
receiver: "either"
|
|
79
|
+
},
|
|
80
|
+
{
|
|
81
|
+
name: "getElementsByName",
|
|
82
|
+
kind: "collection",
|
|
83
|
+
receiver: "document"
|
|
84
|
+
},
|
|
85
|
+
{
|
|
86
|
+
name: "querySelectorAll",
|
|
87
|
+
kind: "collection",
|
|
88
|
+
receiver: "either"
|
|
89
|
+
},
|
|
90
|
+
{
|
|
91
|
+
name: "parentElement",
|
|
92
|
+
kind: "property",
|
|
93
|
+
receiver: "element"
|
|
94
|
+
},
|
|
95
|
+
{
|
|
96
|
+
name: "firstElementChild",
|
|
97
|
+
kind: "property",
|
|
98
|
+
receiver: "element"
|
|
99
|
+
},
|
|
100
|
+
{
|
|
101
|
+
name: "lastElementChild",
|
|
102
|
+
kind: "property",
|
|
103
|
+
receiver: "element"
|
|
104
|
+
},
|
|
105
|
+
{
|
|
106
|
+
name: "nextElementSibling",
|
|
107
|
+
kind: "property",
|
|
108
|
+
receiver: "element"
|
|
109
|
+
},
|
|
110
|
+
{
|
|
111
|
+
name: "previousElementSibling",
|
|
112
|
+
kind: "property",
|
|
113
|
+
receiver: "element"
|
|
114
|
+
},
|
|
115
|
+
{
|
|
116
|
+
name: "offsetParent",
|
|
117
|
+
kind: "property",
|
|
118
|
+
receiver: "element"
|
|
119
|
+
}
|
|
120
|
+
];
|
|
121
|
+
const WAIVER_RE = /^\s*safe-html:\s*\S/u;
|
|
122
|
+
const REACT_HTML_PROPS = [
|
|
123
|
+
{
|
|
124
|
+
name: "dangerouslySetInnerHTML",
|
|
125
|
+
kind: "payload",
|
|
126
|
+
htmlKey: "__html"
|
|
127
|
+
},
|
|
128
|
+
{
|
|
129
|
+
name: "srcDoc",
|
|
130
|
+
kind: "value"
|
|
131
|
+
},
|
|
132
|
+
{
|
|
133
|
+
name: "srcdoc",
|
|
134
|
+
kind: "value"
|
|
135
|
+
}
|
|
136
|
+
];
|
|
137
|
+
const REACT_ELEMENT_FACTORIES = /* @__PURE__ */ new Set([
|
|
138
|
+
"createElement",
|
|
139
|
+
"React.createElement",
|
|
140
|
+
"jsx",
|
|
141
|
+
"jsxs",
|
|
142
|
+
"jsxDEV"
|
|
143
|
+
]);
|
|
144
|
+
const HTML_PROPERTIES = /* @__PURE__ */ new Set([
|
|
145
|
+
"innerHTML",
|
|
146
|
+
"outerHTML",
|
|
147
|
+
"srcdoc"
|
|
148
|
+
]);
|
|
149
|
+
const HTML_FIRST_ARGUMENT_METHODS = /* @__PURE__ */ new Set(["createContextualFragment", "setHTMLUnsafe"]);
|
|
150
|
+
const DOCUMENT_WRITE_METHODS = /* @__PURE__ */ new Set(["write", "writeln"]);
|
|
151
|
+
const DOCUMENT_MEMBERS = /* @__PURE__ */ new Set([
|
|
152
|
+
"contentDocument",
|
|
153
|
+
"document",
|
|
154
|
+
"ownerDocument"
|
|
155
|
+
]);
|
|
4
156
|
const HTML_SANITIZER_LEAF_RE = /^(?:escape(?:html|xml)|sanitize(?:html|dom)|purify(?:html|dom)|dompurify)$/iu;
|
|
5
|
-
const DOM_PURIFY_RE = /^dompurify$/iu;
|
|
6
157
|
const SANITIZER_RESULT_MEMBERS = /* @__PURE__ */ new Set([
|
|
7
158
|
"value",
|
|
8
159
|
"data",
|
|
9
160
|
"html"
|
|
10
161
|
]);
|
|
11
|
-
const
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
162
|
+
const SANITIZER_POLICY = {
|
|
163
|
+
allowlist: "allowlist",
|
|
164
|
+
provenance: "provenance"
|
|
165
|
+
};
|
|
166
|
+
const SANITIZER_POLICY_VALUES = Object.values(SANITIZER_POLICY);
|
|
167
|
+
const sanitizerPolicyFrom = (option) => {
|
|
168
|
+
if (option === void 0) return SANITIZER_POLICY.allowlist;
|
|
169
|
+
if (typeof option !== "object" || option === null || Array.isArray(option)) throw new Error("no-unsafe-inner-html options must be an object");
|
|
170
|
+
const policy = Reflect.get(option, "sanitizerPolicy");
|
|
171
|
+
switch (policy) {
|
|
172
|
+
case void 0:
|
|
173
|
+
case SANITIZER_POLICY.allowlist: return SANITIZER_POLICY.allowlist;
|
|
174
|
+
case SANITIZER_POLICY.provenance: return SANITIZER_POLICY.provenance;
|
|
175
|
+
default: throw new Error(`unknown sanitizer policy: ${String(policy)}`);
|
|
176
|
+
}
|
|
177
|
+
};
|
|
178
|
+
const staticCalleeName = (node) => {
|
|
179
|
+
const expression = unwrapExpression(node);
|
|
180
|
+
if (isIdentifier(expression)) return expression.name;
|
|
181
|
+
if (expression?.type !== "MemberExpression") return null;
|
|
182
|
+
const receiver = staticCalleeName(expression.object);
|
|
183
|
+
const property = memberPropertyName(expression);
|
|
184
|
+
return receiver === null || property === null ? null : `${receiver}.${property}`;
|
|
185
|
+
};
|
|
186
|
+
const isSanitizerCall = (node) => {
|
|
187
|
+
if (!isAstNode(node) || node.type !== "CallExpression") return false;
|
|
188
|
+
const dotted = staticCalleeName(node.callee);
|
|
189
|
+
if (dotted === null) return false;
|
|
15
190
|
const parts = dotted.split(".");
|
|
16
191
|
const leaf = parts.at(-1);
|
|
17
192
|
if (leaf === void 0) return false;
|
|
18
193
|
if (HTML_SANITIZER_LEAF_RE.test(leaf)) return true;
|
|
19
|
-
|
|
20
|
-
return leaf === "sanitize" && DOM_PURIFY_RE.test(objectName ?? "");
|
|
194
|
+
return leaf === "sanitize" && /^dompurify$/iu.test(parts.at(-2) ?? "");
|
|
21
195
|
};
|
|
22
|
-
const
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
196
|
+
const isSanitizerResult = (node) => {
|
|
197
|
+
const expression = unwrapExpression(node);
|
|
198
|
+
if (expression?.type !== "MemberExpression") return false;
|
|
199
|
+
const property = memberPropertyName(expression);
|
|
200
|
+
return property !== null && SANITIZER_RESULT_MEMBERS.has(property) && isSanitizerCall(expression.object);
|
|
201
|
+
};
|
|
202
|
+
const hasLocation = (node) => typeof node === "object" && node !== null && "loc" in node && typeof node.loc === "object" && node.loc !== null && "start" in node.loc && typeof node.loc.start === "object" && node.loc.start !== null && "line" in node.loc.start && typeof node.loc.start.line === "number";
|
|
203
|
+
const startLine = (node) => hasLocation(node) ? node.loc.start.line : NaN;
|
|
204
|
+
const isWaiverComment = (value) => typeof value === "object" && value !== null && "value" in value && typeof value.value === "string" && WAIVER_RE.test(value.value) && "loc" in value && typeof value.loc === "object" && value.loc !== null;
|
|
205
|
+
const jsxAttributeName = (node) => isAstNode(node) && node.type === "JSXIdentifier" && typeof node.name === "string" ? node.name : null;
|
|
206
|
+
const isProvenSafeValue = (node, sanitizerPolicy) => {
|
|
207
|
+
const expression = unwrapExpression(node);
|
|
208
|
+
if (expression === null) return false;
|
|
209
|
+
if (isStringLiteral(expression)) return true;
|
|
210
|
+
if (sanitizerPolicy === SANITIZER_POLICY.allowlist && (isSanitizerCall(expression) || isSanitizerResult(expression))) return true;
|
|
211
|
+
return expression.type === "TemplateLiteral" && Array.isArray(expression.expressions) && expression.expressions.every((entry) => isProvenSafeValue(entry, sanitizerPolicy));
|
|
26
212
|
};
|
|
27
|
-
const
|
|
28
|
-
if (!
|
|
29
|
-
if (
|
|
30
|
-
|
|
31
|
-
if (isSanitizerCall(node)) return true;
|
|
32
|
-
if (node.type === "MemberExpression" && node.computed === false && isProvenSafeMemberLeaf(node)) return true;
|
|
33
|
-
return false;
|
|
213
|
+
const objectPropertyKey = (property) => {
|
|
214
|
+
if (!isAstNode(property)) return null;
|
|
215
|
+
if (property.computed === true) return staticStringValue(property.key);
|
|
216
|
+
return getPropertyName(property.key);
|
|
34
217
|
};
|
|
35
|
-
const
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
218
|
+
const isOpaqueProperty = (property) => property.type === "SpreadElement" || property.type === "Property" && property.computed === true && objectPropertyKey(property) === null;
|
|
219
|
+
const objectProperties = (node) => {
|
|
220
|
+
const expression = unwrapExpression(node);
|
|
221
|
+
if (expression?.type !== "ObjectExpression" || !Array.isArray(expression.properties)) return [];
|
|
222
|
+
return expression.properties.filter(isAstNode);
|
|
39
223
|
};
|
|
40
|
-
const
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
if (
|
|
47
|
-
|
|
48
|
-
|
|
224
|
+
const appliedArgumentsFor = (args) => {
|
|
225
|
+
const applied = unwrapExpression(args.at(1));
|
|
226
|
+
if (applied === null || applied.type === "Literal" && applied.value === null || isIdentifier(applied, "undefined")) return {
|
|
227
|
+
status: "known",
|
|
228
|
+
args: []
|
|
229
|
+
};
|
|
230
|
+
if (applied.type !== "ArrayExpression" || !Array.isArray(applied.elements)) return {
|
|
231
|
+
status: "opaque",
|
|
232
|
+
args: [],
|
|
233
|
+
spread: args.at(1),
|
|
234
|
+
tail: []
|
|
235
|
+
};
|
|
236
|
+
return resolveLiteralArraySpreads(applied.elements);
|
|
237
|
+
};
|
|
238
|
+
const invokedArguments = (call) => {
|
|
239
|
+
if (!isAstNode(call)) return {
|
|
240
|
+
status: "known",
|
|
241
|
+
args: []
|
|
242
|
+
};
|
|
243
|
+
const rawArguments = Array.isArray(call.arguments) ? call.arguments : [];
|
|
244
|
+
const resolved = resolveLiteralArraySpreads(rawArguments);
|
|
245
|
+
const callee = unwrapExpression(call.callee);
|
|
246
|
+
if (callee?.type !== "MemberExpression") return resolved;
|
|
247
|
+
const method = memberPropertyName(callee);
|
|
248
|
+
if (method === "call") return {
|
|
249
|
+
...resolved,
|
|
250
|
+
args: resolved.args.slice(1)
|
|
251
|
+
};
|
|
252
|
+
if (method !== "apply") return resolved;
|
|
253
|
+
if (resolved.status === "opaque") {
|
|
254
|
+
if (resolved.args.length >= 2) return appliedArgumentsFor(resolved.args);
|
|
255
|
+
return {
|
|
256
|
+
...resolved,
|
|
257
|
+
args: [],
|
|
258
|
+
tail: []
|
|
259
|
+
};
|
|
260
|
+
}
|
|
261
|
+
return appliedArgumentsFor(resolved.args);
|
|
262
|
+
};
|
|
263
|
+
const isDocumentReceiver = (node) => {
|
|
264
|
+
const receiver = unwrapExpression(node);
|
|
265
|
+
if (isIdentifier(receiver, "document")) return true;
|
|
266
|
+
if (receiver?.type !== "MemberExpression") return false;
|
|
267
|
+
const name = memberPropertyName(receiver);
|
|
268
|
+
return name !== null && DOCUMENT_MEMBERS.has(name);
|
|
49
269
|
};
|
|
50
270
|
var no_unsafe_inner_html_default = eslintCompatPlugin({
|
|
51
271
|
meta: { name: "no-unsafe-inner-html" },
|
|
52
272
|
rules: { "no-unsafe-inner-html": {
|
|
53
273
|
meta: {
|
|
54
274
|
type: "problem",
|
|
275
|
+
schema: [{
|
|
276
|
+
type: "object",
|
|
277
|
+
properties: { sanitizerPolicy: {
|
|
278
|
+
type: "string",
|
|
279
|
+
enum: SANITIZER_POLICY_VALUES
|
|
280
|
+
} },
|
|
281
|
+
additionalProperties: false
|
|
282
|
+
}],
|
|
55
283
|
messages: {
|
|
56
|
-
unsafeInnerHtml: "
|
|
284
|
+
unsafeInnerHtml: "Dynamic HTML injected into the DOM must be static, proven by the configured sanitizer policy, or have an adjacent `// safe-html: <provenance>` comment naming the trusted source. Function names outside the allowlist are not proof because a local identity function can shadow them.",
|
|
57
285
|
unsafeInnerHtmlSpread: "Do not spread an object into dangerouslySetInnerHTML. Keep `__html` inline so this rule can prove the HTML value is sanitized or escaped."
|
|
58
286
|
}
|
|
59
287
|
},
|
|
60
288
|
createOnce(context) {
|
|
61
|
-
const
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
289
|
+
const isKnownDomTarget = ({ node, scope, seen = /* @__PURE__ */ new Set(), kind = "element" }) => {
|
|
290
|
+
const target = unwrapExpression(node);
|
|
291
|
+
if (isIdentifier(target)) {
|
|
292
|
+
let current = scope;
|
|
293
|
+
while (current !== null) {
|
|
294
|
+
const variable = current.set.get(target.name);
|
|
295
|
+
if (variable === void 0) {
|
|
296
|
+
current = current.upper;
|
|
297
|
+
continue;
|
|
298
|
+
}
|
|
299
|
+
if (variable.defs.length === 0) return kind === "element" && isDocumentReceiver(target);
|
|
300
|
+
if (seen.has(variable)) return false;
|
|
301
|
+
seen.add(variable);
|
|
302
|
+
const definition = variable.defs.at(0);
|
|
303
|
+
if (variable.defs.length !== 1 || definition?.type !== "Variable" || definition.node.type !== "VariableDeclarator" || definition.node.id.type !== "Identifier" || definition.node.init === null || variable.references.some((reference) => reference.isWrite() && !reference.init)) return false;
|
|
304
|
+
return isKnownDomTarget({
|
|
305
|
+
node: definition.node.init,
|
|
306
|
+
scope: context.sourceCode.getScope(definition.node),
|
|
307
|
+
seen,
|
|
308
|
+
kind
|
|
309
|
+
});
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
const hasProducerReceiver = (receiver, producer) => {
|
|
313
|
+
if (producer.receiver !== "element" && isDocumentReceiver(receiver)) return true;
|
|
314
|
+
return producer.receiver !== "document" && isKnownDomTarget({
|
|
315
|
+
node: receiver,
|
|
316
|
+
scope,
|
|
317
|
+
seen
|
|
318
|
+
});
|
|
319
|
+
};
|
|
320
|
+
const isElementCollection = (collection) => isKnownDomTarget({
|
|
321
|
+
node: collection,
|
|
322
|
+
scope,
|
|
323
|
+
seen,
|
|
324
|
+
kind: "collection"
|
|
325
|
+
});
|
|
326
|
+
if (kind === "element" && target?.type === "MemberExpression") {
|
|
327
|
+
const property = memberPropertyName(target);
|
|
328
|
+
const producer = DOM_ELEMENT_PRODUCERS.find((entry) => entry.kind === "property" && entry.name === property);
|
|
329
|
+
if (producer !== void 0) return hasProducerReceiver(target.object, producer);
|
|
330
|
+
const index = unwrapExpression(target.property);
|
|
331
|
+
if (target.computed === true && index?.type === "Literal" && typeof index.value === "number" && Number.isInteger(index.value) && index.value >= 0) return isElementCollection(target.object);
|
|
332
|
+
}
|
|
333
|
+
if (target?.type === "CallExpression") {
|
|
334
|
+
const callee = invokedCallee(target);
|
|
335
|
+
if (callee?.type !== "MemberExpression") return false;
|
|
336
|
+
const method = memberPropertyName(callee);
|
|
337
|
+
if (kind === "element" && method === "item" && isElementCollection(callee.object)) return true;
|
|
338
|
+
const producer = DOM_ELEMENT_PRODUCERS.find((entry) => entry.kind === (kind === "collection" ? "collection" : "method") && entry.name === method);
|
|
339
|
+
return producer !== void 0 && hasProducerReceiver(callee.object, producer);
|
|
340
|
+
}
|
|
341
|
+
return kind === "element" && isDocumentReceiver(target);
|
|
65
342
|
};
|
|
66
|
-
|
|
67
|
-
const
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
343
|
+
let sanitizerPolicy = SANITIZER_POLICY.allowlist;
|
|
344
|
+
const waiverLines = /* @__PURE__ */ new Set();
|
|
345
|
+
const sinks = [];
|
|
346
|
+
const observeSink = (node, kind) => {
|
|
347
|
+
if (isAstNode(node)) sinks.push({
|
|
71
348
|
node,
|
|
72
|
-
|
|
349
|
+
kind
|
|
73
350
|
});
|
|
74
351
|
};
|
|
75
|
-
const
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
352
|
+
const consumeWaiver = (node) => {
|
|
353
|
+
const line = startLine(node) - 1;
|
|
354
|
+
if (!waiverLines.has(line)) return false;
|
|
355
|
+
waiverLines.delete(line);
|
|
356
|
+
return true;
|
|
357
|
+
};
|
|
358
|
+
const reportIfUnsafe = (node) => {
|
|
359
|
+
observeSink(node, isProvenSafeValue(node, sanitizerPolicy) ? "safe" : "unsafe");
|
|
360
|
+
};
|
|
361
|
+
const checkDangerousPayload = (value, htmlKey) => {
|
|
362
|
+
const expression = unwrapExpression(value);
|
|
363
|
+
if (expression?.type !== "ObjectExpression") {
|
|
364
|
+
reportIfUnsafe(value);
|
|
365
|
+
return;
|
|
366
|
+
}
|
|
367
|
+
for (const property of objectProperties(expression)) {
|
|
368
|
+
if (isOpaqueProperty(property)) {
|
|
369
|
+
observeSink(property, "opaque-payload");
|
|
370
|
+
continue;
|
|
371
|
+
}
|
|
372
|
+
if (property.type === "Property" && objectPropertyKey(property) === htmlKey) reportIfUnsafe(property.value);
|
|
373
|
+
}
|
|
374
|
+
};
|
|
375
|
+
const checkReactHtmlProp = (prop, value) => {
|
|
376
|
+
switch (prop.kind) {
|
|
377
|
+
case "payload":
|
|
378
|
+
checkDangerousPayload(value, prop.htmlKey);
|
|
379
|
+
return;
|
|
380
|
+
case "value":
|
|
381
|
+
reportIfUnsafe(value);
|
|
382
|
+
return;
|
|
383
|
+
default: throw new Error(`Unknown React HTML prop: ${String(prop)}`);
|
|
384
|
+
}
|
|
385
|
+
};
|
|
386
|
+
const checkHtmlPropertySources = (sources, domTarget) => {
|
|
387
|
+
for (const source of sources) {
|
|
388
|
+
if (unwrapExpression(source)?.type !== "ObjectExpression") {
|
|
389
|
+
if (domTarget) reportIfUnsafe(source);
|
|
390
|
+
continue;
|
|
391
|
+
}
|
|
392
|
+
for (const property of objectProperties(source)) {
|
|
393
|
+
if (isOpaqueProperty(property)) {
|
|
394
|
+
if (domTarget) reportIfUnsafe(property);
|
|
395
|
+
continue;
|
|
396
|
+
}
|
|
397
|
+
const key = property.type === "Property" ? objectPropertyKey(property) : null;
|
|
398
|
+
if (key !== null && HTML_PROPERTIES.has(key)) reportIfUnsafe(property.value);
|
|
399
|
+
}
|
|
82
400
|
}
|
|
83
401
|
};
|
|
84
402
|
return {
|
|
85
403
|
before() {
|
|
86
|
-
|
|
404
|
+
sanitizerPolicy = sanitizerPolicyFrom(context.options.at(0));
|
|
405
|
+
waiverLines.clear();
|
|
406
|
+
sinks.length = 0;
|
|
407
|
+
},
|
|
408
|
+
"Program:exit"() {
|
|
409
|
+
sinks.sort((left, right) => left.node.range[0] - right.node.range[0]);
|
|
410
|
+
for (const { node, kind } of sinks) {
|
|
411
|
+
const waived = consumeWaiver(node);
|
|
412
|
+
if (kind === "safe" || kind === "unsafe" && waived) continue;
|
|
413
|
+
context.report({
|
|
414
|
+
node,
|
|
415
|
+
messageId: kind === "opaque-payload" ? "unsafeInnerHtmlSpread" : "unsafeInnerHtml"
|
|
416
|
+
});
|
|
417
|
+
}
|
|
418
|
+
sinks.length = 0;
|
|
87
419
|
},
|
|
88
420
|
Program(node) {
|
|
89
|
-
|
|
421
|
+
const comments = Array.isArray(node.comments) ? node.comments : [];
|
|
422
|
+
for (const comment of comments) if (isWaiverComment(comment)) waiverLines.add(comment.loc.end.line);
|
|
90
423
|
},
|
|
91
424
|
JSXAttribute(node) {
|
|
92
|
-
|
|
93
|
-
const
|
|
94
|
-
if (
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
if (expression?.type === "ObjectExpression") {
|
|
98
|
-
reportPayloadSpreads(expression);
|
|
99
|
-
return;
|
|
100
|
-
}
|
|
101
|
-
reportIfUnsafe(value.expression);
|
|
425
|
+
const name = jsxAttributeName(node.name);
|
|
426
|
+
const prop = REACT_HTML_PROPS.find((entry) => entry.name === name);
|
|
427
|
+
if (prop === void 0) return;
|
|
428
|
+
const value = unwrapExpression(node.value);
|
|
429
|
+
checkReactHtmlProp(prop, value?.type === "JSXExpressionContainer" ? value.expression : value);
|
|
102
430
|
},
|
|
103
431
|
Property(node) {
|
|
104
|
-
|
|
105
|
-
|
|
432
|
+
const key = objectPropertyKey(node);
|
|
433
|
+
const prop = REACT_HTML_PROPS.find((entry) => entry.name === key);
|
|
434
|
+
if (prop?.kind !== "payload") return;
|
|
435
|
+
const container = node.parent;
|
|
436
|
+
if (isAstNode(container) && container.type === "ObjectExpression") checkReactHtmlProp(prop, node.value);
|
|
106
437
|
},
|
|
107
438
|
AssignmentExpression(node) {
|
|
108
|
-
const target = node.left;
|
|
109
|
-
if (target
|
|
110
|
-
|
|
439
|
+
const target = unwrapExpression(node.left);
|
|
440
|
+
if (target?.type !== "MemberExpression") return;
|
|
441
|
+
const name = memberPropertyName(target);
|
|
442
|
+
if (name !== null && HTML_PROPERTIES.has(name)) reportIfUnsafe(node.right);
|
|
443
|
+
},
|
|
444
|
+
CallExpression(node) {
|
|
445
|
+
const call = node;
|
|
446
|
+
const callee = isAstNode(call) ? invokedCallee(call) : null;
|
|
447
|
+
const invoked = invokedArguments(node);
|
|
448
|
+
const factory = staticCalleeName(callee);
|
|
449
|
+
if (factory !== null && REACT_ELEMENT_FACTORIES.has(factory)) {
|
|
450
|
+
for (const property of objectProperties(invoked.args.at(1))) {
|
|
451
|
+
if (property.type !== "Property") continue;
|
|
452
|
+
const key = objectPropertyKey(property);
|
|
453
|
+
const prop = REACT_HTML_PROPS.find((entry) => entry.name === key);
|
|
454
|
+
if (prop?.kind === "value") checkReactHtmlProp(prop, property.value);
|
|
455
|
+
}
|
|
456
|
+
return;
|
|
457
|
+
}
|
|
458
|
+
if (callee?.type !== "MemberExpression") return;
|
|
459
|
+
const method = memberPropertyName(callee);
|
|
460
|
+
if (method === null) return;
|
|
461
|
+
if (HTML_FIRST_ARGUMENT_METHODS.has(method)) {
|
|
462
|
+
if (invoked.status === "opaque") {
|
|
463
|
+
reportIfUnsafe(node);
|
|
464
|
+
return;
|
|
465
|
+
}
|
|
466
|
+
reportIfUnsafe(invoked.args.at(0));
|
|
467
|
+
return;
|
|
468
|
+
}
|
|
469
|
+
if (method === "insertAdjacentHTML") {
|
|
470
|
+
if (invoked.status === "opaque") {
|
|
471
|
+
reportIfUnsafe(node);
|
|
472
|
+
return;
|
|
473
|
+
}
|
|
474
|
+
reportIfUnsafe(invoked.args.at(1));
|
|
475
|
+
return;
|
|
476
|
+
}
|
|
477
|
+
if (method === "setAttribute") {
|
|
478
|
+
if (invoked.status === "opaque") {
|
|
479
|
+
reportIfUnsafe(node);
|
|
480
|
+
return;
|
|
481
|
+
}
|
|
482
|
+
if (staticStringValue(invoked.args.at(0))?.toLowerCase() === "srcdoc") reportIfUnsafe(invoked.args.at(1));
|
|
483
|
+
return;
|
|
484
|
+
}
|
|
485
|
+
if (DOCUMENT_WRITE_METHODS.has(method) && isDocumentReceiver(callee.object)) {
|
|
486
|
+
if (invoked.status === "opaque") {
|
|
487
|
+
reportIfUnsafe(node);
|
|
488
|
+
return;
|
|
489
|
+
}
|
|
490
|
+
observeSink(node, invoked.args.every((argument) => isProvenSafeValue(argument, sanitizerPolicy)) ? "safe" : "unsafe");
|
|
491
|
+
return;
|
|
492
|
+
}
|
|
493
|
+
if (isIdentifier(callee.object, "Object") && method === "assign") {
|
|
494
|
+
const domTarget = isKnownDomTarget({
|
|
495
|
+
node: invoked.args.at(0),
|
|
496
|
+
scope: context.sourceCode.getScope(node)
|
|
497
|
+
});
|
|
498
|
+
checkHtmlPropertySources([...invoked.args.slice(1), ...invoked.status === "opaque" ? invoked.tail : []], domTarget);
|
|
499
|
+
if (invoked.status === "opaque" && domTarget) reportIfUnsafe(invoked.spread);
|
|
500
|
+
}
|
|
111
501
|
}
|
|
112
502
|
};
|
|
113
503
|
}
|
|
114
504
|
} }
|
|
115
505
|
});
|
|
116
506
|
//#endregion
|
|
117
|
-
export { no_unsafe_inner_html_default as default };
|
|
507
|
+
export { DOM_ELEMENT_PRODUCERS, REACT_HTML_PROPS, no_unsafe_inner_html_default as default };
|
|
118
508
|
|
|
119
509
|
//# sourceMappingURL=no-unsafe-inner-html.mjs.map
|