oxlint-plugin-react-doctor 0.5.5 → 0.5.6
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.d.ts +999 -0
- package/dist/index.js +557 -4
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -323,9 +323,18 @@ const BROWSER_ARTIFACT_PATH_PATTERNS = [
|
|
|
323
323
|
const AGENT_TOOL_DANGEROUS_CAPABILITY_PATTERN = /\b(?:exec|execSync|spawn|child_process|eval|new Function|vm\.run|readFile|writeFile|fs\.read|fs\.write|fetch|axios|http\.request|sandbox|runCode|executeCode)\b/;
|
|
324
324
|
//#endregion
|
|
325
325
|
//#region src/plugin/rules/security-scan/utils/is-browser-artifact-path.ts
|
|
326
|
-
const
|
|
326
|
+
const SERVER_BUILD_ROOT_SEGMENTS = new Set([".next", ".output"]);
|
|
327
|
+
const isNonShippedBuildArtifactPath = (relativePath) => {
|
|
328
|
+
const segments = relativePath.split("/");
|
|
329
|
+
for (let index = 0; index < segments.length; index += 1) {
|
|
330
|
+
if (!SERVER_BUILD_ROOT_SEGMENTS.has(segments[index])) continue;
|
|
331
|
+
if (segments[index] === ".next" && segments[index + 1] === "dev") return true;
|
|
332
|
+
if (segments[index + 1] === "server" && index + 2 < segments.length) return true;
|
|
333
|
+
}
|
|
334
|
+
return false;
|
|
335
|
+
};
|
|
327
336
|
const isBrowserArtifactPath = (relativePath, isGeneratedBundle) => {
|
|
328
|
-
if (
|
|
337
|
+
if (isNonShippedBuildArtifactPath(relativePath)) return false;
|
|
329
338
|
if (isGeneratedBundle) return true;
|
|
330
339
|
if (relativePath.endsWith(".map")) return true;
|
|
331
340
|
return BROWSER_ARTIFACT_PATH_PATTERNS.some((pattern) => pattern.test(relativePath));
|
|
@@ -1108,6 +1117,11 @@ const getImportedNameFromModule = (contextNode, localIdentifierName, moduleSourc
|
|
|
1108
1117
|
if (info.source !== moduleSource) return null;
|
|
1109
1118
|
return info.imported;
|
|
1110
1119
|
};
|
|
1120
|
+
const getImportSourceForName = (contextNode, localIdentifierName) => {
|
|
1121
|
+
const lookup = getImportLookup(contextNode);
|
|
1122
|
+
if (!lookup) return null;
|
|
1123
|
+
return lookup.get(localIdentifierName)?.source ?? null;
|
|
1124
|
+
};
|
|
1111
1125
|
//#endregion
|
|
1112
1126
|
//#region src/plugin/utils/find-variable-initializer.ts
|
|
1113
1127
|
const FUNCTION_LIKE_TYPES$1 = new Set([
|
|
@@ -8495,6 +8509,136 @@ const insecureCryptoRisk = defineRule({
|
|
|
8495
8509
|
}
|
|
8496
8510
|
});
|
|
8497
8511
|
//#endregion
|
|
8512
|
+
//#region src/plugin/rules/security-scan/utils/find-matching-bracket.ts
|
|
8513
|
+
const findMatchingBracket = (content, openIndex) => {
|
|
8514
|
+
const open = content[openIndex];
|
|
8515
|
+
const close = open === "(" ? ")" : open === "{" ? "}" : open === "[" ? "]" : "";
|
|
8516
|
+
if (close === "") return -1;
|
|
8517
|
+
let depth = 0;
|
|
8518
|
+
let stringDelimiter = null;
|
|
8519
|
+
for (let index = openIndex; index < content.length; index += 1) {
|
|
8520
|
+
const character = content[index];
|
|
8521
|
+
if (stringDelimiter !== null) {
|
|
8522
|
+
if (character === "\\") index += 1;
|
|
8523
|
+
else if (character === stringDelimiter) stringDelimiter = null;
|
|
8524
|
+
continue;
|
|
8525
|
+
}
|
|
8526
|
+
if (character === "\"" || character === "'" || character === "`") stringDelimiter = character;
|
|
8527
|
+
else if (character === open) depth += 1;
|
|
8528
|
+
else if (character === close) {
|
|
8529
|
+
depth -= 1;
|
|
8530
|
+
if (depth === 0) return index;
|
|
8531
|
+
}
|
|
8532
|
+
}
|
|
8533
|
+
return -1;
|
|
8534
|
+
};
|
|
8535
|
+
//#endregion
|
|
8536
|
+
//#region src/plugin/rules/security-scan/insecure-session-cookie.ts
|
|
8537
|
+
const AUTH_COOKIE_NAME_TOKEN = `(?<![A-Za-z0-9])(?:session|sess|sid|connect\\.sid|auth|jwt|access[_-]?token|refresh[_-]?token|id[_-]?token)(?![A-Za-z0-9])`;
|
|
8538
|
+
const AUTH_COOKIE_NAME_LITERAL = `[\`"'][^\`"']*?${AUTH_COOKIE_NAME_TOKEN}[^\`"']*[\`"']`;
|
|
8539
|
+
const AUTH_COOKIE_SET_CALL_PATTERN = new RegExp(`(?:\\.cookies\\.set|cookies\\(\\s*\\)\\.set|\\.cookie)\\s*\\(\\s*${AUTH_COOKIE_NAME_LITERAL}`, "gi");
|
|
8540
|
+
const HTTP_ONLY_DISABLED_PATTERN = /httpOnly\s*:\s*false\b/i;
|
|
8541
|
+
const STRING_LITERAL_PATTERN = /"(?:[^"\\]|\\.)*"|'(?:[^'\\]|\\.)*'|`(?:[^`\\]|\\.)*`/g;
|
|
8542
|
+
const blankStringContents = (text) => {
|
|
8543
|
+
const characters = text.split("");
|
|
8544
|
+
let index = 0;
|
|
8545
|
+
let stringDelimiter = null;
|
|
8546
|
+
while (index < text.length) {
|
|
8547
|
+
const character = text[index];
|
|
8548
|
+
if (stringDelimiter !== null) {
|
|
8549
|
+
if (character === "\\") {
|
|
8550
|
+
index += 2;
|
|
8551
|
+
continue;
|
|
8552
|
+
}
|
|
8553
|
+
if (character === stringDelimiter) stringDelimiter = null;
|
|
8554
|
+
else if (character !== "\n") characters[index] = " ";
|
|
8555
|
+
index += 1;
|
|
8556
|
+
continue;
|
|
8557
|
+
}
|
|
8558
|
+
if (character === "\"" || character === "'" || character === "`") stringDelimiter = character;
|
|
8559
|
+
index += 1;
|
|
8560
|
+
}
|
|
8561
|
+
return characters.join("");
|
|
8562
|
+
};
|
|
8563
|
+
const COOKIE_CONFIG_OPENER_PATTERN = /cookie\s*:\s*\{/gi;
|
|
8564
|
+
const CLIENT_AUTH_COOKIE_WRITE_PATTERN = new RegExp(`document\\.cookie\\s*=\\s*[\`"'][^\`"'=;]*?${AUTH_COOKIE_NAME_TOKEN}[^\`"'=;]*=`, "gi");
|
|
8565
|
+
const countTopLevelArguments = (argumentsSource) => {
|
|
8566
|
+
if (argumentsSource.trim().length === 0) return 0;
|
|
8567
|
+
let depth = 0;
|
|
8568
|
+
let stringDelimiter = null;
|
|
8569
|
+
let count = 1;
|
|
8570
|
+
for (let index = 0; index < argumentsSource.length; index += 1) {
|
|
8571
|
+
const character = argumentsSource[index];
|
|
8572
|
+
if (stringDelimiter !== null) {
|
|
8573
|
+
if (character === "\\") index += 1;
|
|
8574
|
+
else if (character === stringDelimiter) stringDelimiter = null;
|
|
8575
|
+
continue;
|
|
8576
|
+
}
|
|
8577
|
+
if (character === "\"" || character === "'" || character === "`") stringDelimiter = character;
|
|
8578
|
+
else if (character === "(" || character === "[" || character === "{") depth += 1;
|
|
8579
|
+
else if (character === ")" || character === "]" || character === "}") depth -= 1;
|
|
8580
|
+
else if (character === "," && depth === 0) count += 1;
|
|
8581
|
+
}
|
|
8582
|
+
return count;
|
|
8583
|
+
};
|
|
8584
|
+
const addMatchFindings = (content, pattern, message, isInsecure, findings) => {
|
|
8585
|
+
pattern.lastIndex = 0;
|
|
8586
|
+
for (let match = pattern.exec(content); match !== null; match = pattern.exec(content)) {
|
|
8587
|
+
if (!isInsecure(match.index, match[0])) continue;
|
|
8588
|
+
const location = getLocationAtIndex(content, match.index);
|
|
8589
|
+
findings.push({
|
|
8590
|
+
message,
|
|
8591
|
+
line: location.line,
|
|
8592
|
+
column: location.column
|
|
8593
|
+
});
|
|
8594
|
+
}
|
|
8595
|
+
};
|
|
8596
|
+
const insecureSessionCookie = defineRule({
|
|
8597
|
+
id: "insecure-session-cookie",
|
|
8598
|
+
title: "Auth cookie missing HttpOnly protection",
|
|
8599
|
+
severity: "warn",
|
|
8600
|
+
recommendation: "Set auth/session cookies server-side with `httpOnly: true`, `secure: true`, and `sameSite`. Cookies set via `document.cookie` or with `httpOnly: false` are readable by any XSS payload and can be stolen.",
|
|
8601
|
+
scan: (file) => {
|
|
8602
|
+
if (!isProductionSourcePath(file.relativePath)) return [];
|
|
8603
|
+
const content = getScannableContent(file);
|
|
8604
|
+
if (!/cookie/i.test(content)) return [];
|
|
8605
|
+
const findings = [];
|
|
8606
|
+
const message = "An auth/session cookie is exposed to JavaScript (set via document.cookie, with httpOnly: false, or without cookie options), letting an XSS payload steal it.";
|
|
8607
|
+
AUTH_COOKIE_SET_CALL_PATTERN.lastIndex = 0;
|
|
8608
|
+
for (let match = AUTH_COOKIE_SET_CALL_PATTERN.exec(content); match !== null; match = AUTH_COOKIE_SET_CALL_PATTERN.exec(content)) {
|
|
8609
|
+
const openParenIndex = match.index + match[0].lastIndexOf("(");
|
|
8610
|
+
const closeParenIndex = findMatchingBracket(content, openParenIndex);
|
|
8611
|
+
if (closeParenIndex < 0) continue;
|
|
8612
|
+
const argumentsSource = content.slice(openParenIndex + 1, closeParenIndex);
|
|
8613
|
+
const hasNoOptions = countTopLevelArguments(argumentsSource) < 3;
|
|
8614
|
+
const argumentsWithoutStrings = argumentsSource.replace(STRING_LITERAL_PATTERN, "");
|
|
8615
|
+
if (!hasNoOptions && !HTTP_ONLY_DISABLED_PATTERN.test(argumentsWithoutStrings)) continue;
|
|
8616
|
+
const location = getLocationAtIndex(content, match.index);
|
|
8617
|
+
findings.push({
|
|
8618
|
+
message,
|
|
8619
|
+
line: location.line,
|
|
8620
|
+
column: location.column
|
|
8621
|
+
});
|
|
8622
|
+
}
|
|
8623
|
+
const blankedContent = blankStringContents(content);
|
|
8624
|
+
COOKIE_CONFIG_OPENER_PATTERN.lastIndex = 0;
|
|
8625
|
+
for (let match = COOKIE_CONFIG_OPENER_PATTERN.exec(blankedContent); match !== null; match = COOKIE_CONFIG_OPENER_PATTERN.exec(blankedContent)) {
|
|
8626
|
+
const braceIndex = match.index + match[0].length - 1;
|
|
8627
|
+
const closeBraceIndex = findMatchingBracket(blankedContent, braceIndex);
|
|
8628
|
+
const block = closeBraceIndex >= 0 ? blankedContent.slice(braceIndex, closeBraceIndex) : blankedContent.slice(braceIndex, braceIndex + 400);
|
|
8629
|
+
if (!HTTP_ONLY_DISABLED_PATTERN.test(block)) continue;
|
|
8630
|
+
const location = getLocationAtIndex(blankedContent, match.index);
|
|
8631
|
+
findings.push({
|
|
8632
|
+
message,
|
|
8633
|
+
line: location.line,
|
|
8634
|
+
column: location.column
|
|
8635
|
+
});
|
|
8636
|
+
}
|
|
8637
|
+
addMatchFindings(content, CLIENT_AUTH_COOKIE_WRITE_PATTERN, message, () => true, findings);
|
|
8638
|
+
return findings;
|
|
8639
|
+
}
|
|
8640
|
+
});
|
|
8641
|
+
//#endregion
|
|
8498
8642
|
//#region src/plugin/constants/event-handlers.ts
|
|
8499
8643
|
const MOUSE_EVENT_HANDLERS = [
|
|
8500
8644
|
"onClick",
|
|
@@ -12700,11 +12844,70 @@ const jsxPropsNoSpreading = defineRule({
|
|
|
12700
12844
|
}
|
|
12701
12845
|
});
|
|
12702
12846
|
//#endregion
|
|
12847
|
+
//#region src/plugin/rules/security-scan/jwt-insecure-verification.ts
|
|
12848
|
+
const NONE_ALGORITHM_PATTERN = /\b(?:alg|algorithms?)\s*:\s*\[?\s*["'`]none["'`]/gi;
|
|
12849
|
+
const isIndexInsideStringLiteral = (content, index) => {
|
|
12850
|
+
let stringDelimiter = null;
|
|
12851
|
+
const templateExpressionDepths = [];
|
|
12852
|
+
for (let cursor = 0; cursor < index; cursor += 1) {
|
|
12853
|
+
const character = content[cursor];
|
|
12854
|
+
if (stringDelimiter === "`") {
|
|
12855
|
+
if (character === "\\") cursor += 1;
|
|
12856
|
+
else if (character === "`") stringDelimiter = null;
|
|
12857
|
+
else if (character === "$" && content[cursor + 1] === "{") {
|
|
12858
|
+
templateExpressionDepths.push(0);
|
|
12859
|
+
stringDelimiter = null;
|
|
12860
|
+
cursor += 1;
|
|
12861
|
+
}
|
|
12862
|
+
continue;
|
|
12863
|
+
}
|
|
12864
|
+
if (stringDelimiter !== null) {
|
|
12865
|
+
if (character === "\\") cursor += 1;
|
|
12866
|
+
else if (character === stringDelimiter) stringDelimiter = null;
|
|
12867
|
+
continue;
|
|
12868
|
+
}
|
|
12869
|
+
if (character === "\"" || character === "'" || character === "`") stringDelimiter = character;
|
|
12870
|
+
else if (templateExpressionDepths.length > 0) {
|
|
12871
|
+
const top = templateExpressionDepths.length - 1;
|
|
12872
|
+
if (character === "{") templateExpressionDepths[top] += 1;
|
|
12873
|
+
else if (character === "}") if (templateExpressionDepths[top] === 0) {
|
|
12874
|
+
templateExpressionDepths.pop();
|
|
12875
|
+
stringDelimiter = "`";
|
|
12876
|
+
} else templateExpressionDepths[top] -= 1;
|
|
12877
|
+
}
|
|
12878
|
+
}
|
|
12879
|
+
return stringDelimiter !== null;
|
|
12880
|
+
};
|
|
12881
|
+
const jwtInsecureVerification = defineRule({
|
|
12882
|
+
id: "jwt-insecure-verification",
|
|
12883
|
+
title: "JWT verified with the 'none' algorithm",
|
|
12884
|
+
severity: "error",
|
|
12885
|
+
recommendation: "Never accept the `none` algorithm; it disables signature verification and lets any forged token through. Pin the real algorithm(s) explicitly (`jwt.verify(token, key, { algorithms: ['RS256'] })`).",
|
|
12886
|
+
scan: (file) => {
|
|
12887
|
+
if (!isProductionSourcePath(file.relativePath)) return [];
|
|
12888
|
+
const content = getScannableContent(file);
|
|
12889
|
+
if (!/\bjwt\b|jsonwebtoken|\bjose\b/i.test(content)) return [];
|
|
12890
|
+
const findings = [];
|
|
12891
|
+
NONE_ALGORITHM_PATTERN.lastIndex = 0;
|
|
12892
|
+
for (let noneMatch = NONE_ALGORITHM_PATTERN.exec(content); noneMatch !== null; noneMatch = NONE_ALGORITHM_PATTERN.exec(content)) {
|
|
12893
|
+
if (isIndexInsideStringLiteral(content, noneMatch.index)) continue;
|
|
12894
|
+
const location = getLocationAtIndex(content, noneMatch.index);
|
|
12895
|
+
findings.push({
|
|
12896
|
+
message: "JWT is configured with the 'none' algorithm, which disables signature verification, so any forged token is accepted.",
|
|
12897
|
+
line: location.line,
|
|
12898
|
+
column: location.column
|
|
12899
|
+
});
|
|
12900
|
+
}
|
|
12901
|
+
return findings;
|
|
12902
|
+
}
|
|
12903
|
+
});
|
|
12904
|
+
//#endregion
|
|
12703
12905
|
//#region src/plugin/rules/security-scan/key-lifecycle-risk.ts
|
|
12704
12906
|
const keyLifecycleRisk = defineRule({
|
|
12705
12907
|
id: "key-lifecycle-risk",
|
|
12706
12908
|
title: "Long-lived key material in repository",
|
|
12707
12909
|
severity: "error",
|
|
12910
|
+
committedFilesOnly: true,
|
|
12708
12911
|
recommendation: "Remove private keys from source, rotate exposed credentials, prefer short-lived deploy credentials, and document revocation/expiry for release keys.",
|
|
12709
12912
|
scan: scanByPattern({
|
|
12710
12913
|
shouldScan: (file) => !TEST_CONTEXT_PATTERN.test(file.relativePath) && !DOCUMENTATION_CONTEXT_PATTERN.test(file.relativePath),
|
|
@@ -27035,6 +27238,8 @@ const publicEnvSecretName = defineRule({
|
|
|
27035
27238
|
});
|
|
27036
27239
|
//#endregion
|
|
27037
27240
|
//#region src/plugin/rules/tanstack-query/query-destructure-result.ts
|
|
27241
|
+
const TANSTACK_QUERY_PACKAGE_PATTERN = /^@tanstack\/[\w-]*query[\w-]*$/;
|
|
27242
|
+
const isTanstackQuerySource = (source) => TANSTACK_QUERY_PACKAGE_PATTERN.test(source) || source === "react-query";
|
|
27038
27243
|
const queryDestructureResult = defineRule({
|
|
27039
27244
|
id: "query-destructure-result",
|
|
27040
27245
|
title: "Whole query result subscribes to every field",
|
|
@@ -27047,6 +27252,8 @@ const queryDestructureResult = defineRule({
|
|
|
27047
27252
|
if (!node.init || !isNodeOfType(node.init, "CallExpression")) return;
|
|
27048
27253
|
const calleeName = isNodeOfType(node.init.callee, "Identifier") ? node.init.callee.name : null;
|
|
27049
27254
|
if (!calleeName || !TANSTACK_QUERY_HOOKS.has(calleeName)) return;
|
|
27255
|
+
const importSource = getImportSourceForName(node, calleeName);
|
|
27256
|
+
if (importSource !== null && !isTanstackQuerySource(importSource)) return;
|
|
27050
27257
|
context.report({
|
|
27051
27258
|
node: node.id,
|
|
27052
27259
|
message: `Destructure ${calleeName}() results instead of assigning the whole query object, so TanStack Query only subscribes to the fields you use.`
|
|
@@ -27889,6 +28096,7 @@ const repositorySecretFile = defineRule({
|
|
|
27889
28096
|
id: "repository-secret-file",
|
|
27890
28097
|
title: "Secret file checked into repository",
|
|
27891
28098
|
severity: "error",
|
|
28099
|
+
committedFilesOnly: true,
|
|
27892
28100
|
recommendation: "Remove committed env files, service-account credentials, npm auth tokens, and webhook URLs; rotate exposed values and keep only redacted examples in source.",
|
|
27893
28101
|
scan: (file) => {
|
|
27894
28102
|
if (!isRepositorySecretFilePath(file.relativePath)) return [];
|
|
@@ -27905,6 +28113,20 @@ const repositorySecretFile = defineRule({
|
|
|
27905
28113
|
}
|
|
27906
28114
|
});
|
|
27907
28115
|
//#endregion
|
|
28116
|
+
//#region src/plugin/rules/security-scan/request-body-mass-assignment.ts
|
|
28117
|
+
const REQUEST_INPUT_SOURCE = "(?:req|request|ctx\\.req|ctx\\.request)\\.(?:body|query|params)|await\\s+(?:req|request)\\.json\\(\\s*\\)";
|
|
28118
|
+
const requestBodyMassAssignment = defineRule({
|
|
28119
|
+
id: "request-body-mass-assignment",
|
|
28120
|
+
title: "Request input spread without field allowlist",
|
|
28121
|
+
severity: "warn",
|
|
28122
|
+
recommendation: "Assign explicit, allowlisted fields (or validate with a strict schema and no `.passthrough()`) instead of spreading/merging request input. Otherwise the client can set ownership, role, or price columns (mass assignment) or pollute the prototype.",
|
|
28123
|
+
scan: scanByPattern({
|
|
28124
|
+
shouldScan: (file) => isProductionSourcePath(file.relativePath),
|
|
28125
|
+
pattern: [new RegExp(`\\.\\.\\.\\s*(?:${REQUEST_INPUT_SOURCE})`, "i"), new RegExp(`(?:Object\\.assign\\s*\\(|_\\.(?:merge|mergeWith|defaultsDeep)\\s*\\(|(?:^|[^.\\w])(?:merge|defaultsDeep)\\s*\\()[\\s\\S]{0,80}?(?:${REQUEST_INPUT_SOURCE})`, "i")],
|
|
28126
|
+
message: "Request input is spread or merged into an object without a field allowlist, enabling mass assignment (client-set owner/role/price fields) or prototype pollution."
|
|
28127
|
+
})
|
|
28128
|
+
});
|
|
28129
|
+
//#endregion
|
|
27908
28130
|
//#region src/plugin/utils/function-body-has-return-with-value.ts
|
|
27909
28131
|
const functionBodyHasReturnWithValue = (functionNode) => {
|
|
27910
28132
|
if (functionNode.type === "ArrowFunctionExpression" && "body" in functionNode) {
|
|
@@ -34330,6 +34552,17 @@ const scope = defineRule({
|
|
|
34330
34552
|
});
|
|
34331
34553
|
} })
|
|
34332
34554
|
});
|
|
34555
|
+
const secretInFallback = defineRule({
|
|
34556
|
+
id: "secret-in-fallback",
|
|
34557
|
+
title: "Hardcoded secret fallback for env var",
|
|
34558
|
+
severity: "error",
|
|
34559
|
+
recommendation: "Remove the literal fallback and fail closed (throw when the variable is unset). The hardcoded value is a committed secret, and the `??`/`||` default makes the app run with it in any environment that forgot to set the var.",
|
|
34560
|
+
scan: scanByPattern({
|
|
34561
|
+
shouldScan: (file) => isProductionSourcePath(file.relativePath),
|
|
34562
|
+
pattern: /\bprocess\.env\.(?![A-Z0-9_]*(?:PUBLIC|PUBLISHABLE|ANON)\b)[A-Z][A-Z0-9_]*(?:SECRET|TOKEN|PASSWORD|PASSWD|PRIVATE_KEY|API_?KEY|APIKEY|ACCESS_KEY|CLIENT_SECRET|CREDENTIAL|SIGNING_KEY|ENCRYPTION_KEY|WEBHOOK_SECRET|SERVICE_ROLE)[A-Z0-9_]*(?<!_(?:NAME|HEADER|ENDPOINT|URL|URI|ID|PREFIX|SUFFIX|PARAM|PARAMS|FIELD|ISSUER|AUDIENCE|ALGORITHM|ALG|REGION|BUCKET|HOST|HOSTNAME|PORT|PATH|VERSION|SCOPE|TYPE|FORMAT|EXPIRY|TTL))\s*(?:\?\?|\|\|)\s*(["'`])(?!(?:changeme|change[_-]?me|placeholder|your[_-]|example|sample|dummy|development|local|todo|replace[_-]?me|https?:\/\/|x{3,}|\*{3,}))[^"'`\n]{8,}\1/i,
|
|
34563
|
+
message: "A secret env var has a hardcoded string fallback: the literal is a committed secret and the app fails open (uses it) when the variable is unset."
|
|
34564
|
+
})
|
|
34565
|
+
});
|
|
34333
34566
|
//#endregion
|
|
34334
34567
|
//#region src/plugin/rules/react-builtins/self-closing-comp.ts
|
|
34335
34568
|
const MESSAGE$2 = "This tag has no children, so the closing tag adds noise without changing output.";
|
|
@@ -35139,8 +35372,11 @@ const supabaseClientOwnedAuthzField = defineRule({
|
|
|
35139
35372
|
})
|
|
35140
35373
|
});
|
|
35141
35374
|
//#endregion
|
|
35375
|
+
//#region src/plugin/rules/security-scan/utils/is-supabase-migration-path.ts
|
|
35376
|
+
const isSupabaseMigrationPath = (relativePath) => /(?:^|\/)supabase\/(?:migrations|schemas)\//.test(relativePath);
|
|
35377
|
+
//#endregion
|
|
35142
35378
|
//#region src/plugin/rules/security-scan/utils/is-sql-path.ts
|
|
35143
|
-
const isSqlPath = (relativePath) => relativePath.endsWith(".sql") ||
|
|
35379
|
+
const isSqlPath = (relativePath) => relativePath.endsWith(".sql") || isSupabaseMigrationPath(relativePath);
|
|
35144
35380
|
const supabaseRlsPolicyRisk = defineRule({
|
|
35145
35381
|
id: "supabase-rls-policy-risk",
|
|
35146
35382
|
title: "Permissive Supabase RLS policy",
|
|
@@ -35158,6 +35394,210 @@ const supabaseRlsPolicyRisk = defineRule({
|
|
|
35158
35394
|
})
|
|
35159
35395
|
});
|
|
35160
35396
|
//#endregion
|
|
35397
|
+
//#region src/plugin/rules/security-scan/utils/sanitize-sql-for-scan.ts
|
|
35398
|
+
const DOLLAR_QUOTE_TAG_PATTERN = /^\$[A-Za-z_]?\w*\$/;
|
|
35399
|
+
const CODE_BODY_KEYWORDS = new Set([
|
|
35400
|
+
"do",
|
|
35401
|
+
"plpgsql",
|
|
35402
|
+
"sql",
|
|
35403
|
+
"plpython3u",
|
|
35404
|
+
"plpythonu",
|
|
35405
|
+
"plperl",
|
|
35406
|
+
"plperlu",
|
|
35407
|
+
"plv8"
|
|
35408
|
+
]);
|
|
35409
|
+
const precedingKeyword = (content, beforeIndex) => {
|
|
35410
|
+
let lookBack = beforeIndex - 1;
|
|
35411
|
+
while (lookBack >= 0 && /\s/.test(content[lookBack] ?? "")) lookBack -= 1;
|
|
35412
|
+
let wordStart = lookBack;
|
|
35413
|
+
while (wordStart >= 0 && /[A-Za-z0-9_]/.test(content[wordStart] ?? "")) wordStart -= 1;
|
|
35414
|
+
return content.slice(wordStart + 1, lookBack + 1).toLowerCase();
|
|
35415
|
+
};
|
|
35416
|
+
const blankCodeBodyInterior = (content, characters, start, end) => {
|
|
35417
|
+
let index = start;
|
|
35418
|
+
let inExecuteStatement = false;
|
|
35419
|
+
while (index < end) {
|
|
35420
|
+
const character = content[index];
|
|
35421
|
+
if (character === ";") {
|
|
35422
|
+
inExecuteStatement = false;
|
|
35423
|
+
index += 1;
|
|
35424
|
+
continue;
|
|
35425
|
+
}
|
|
35426
|
+
if (/[A-Za-z_]/.test(character)) {
|
|
35427
|
+
let wordEnd = index;
|
|
35428
|
+
while (wordEnd < end && /[A-Za-z0-9_]/.test(content[wordEnd] ?? "")) wordEnd += 1;
|
|
35429
|
+
if (content.slice(index, wordEnd).toLowerCase() === "execute") inExecuteStatement = true;
|
|
35430
|
+
index = wordEnd;
|
|
35431
|
+
continue;
|
|
35432
|
+
}
|
|
35433
|
+
if (character === "'") {
|
|
35434
|
+
const keepVisible = inExecuteStatement;
|
|
35435
|
+
if (!keepVisible) characters[index] = " ";
|
|
35436
|
+
index += 1;
|
|
35437
|
+
while (index < end) {
|
|
35438
|
+
if (content[index] === "'") {
|
|
35439
|
+
if (content[index + 1] === "'") {
|
|
35440
|
+
if (!keepVisible) {
|
|
35441
|
+
characters[index] = " ";
|
|
35442
|
+
characters[index + 1] = " ";
|
|
35443
|
+
}
|
|
35444
|
+
index += 2;
|
|
35445
|
+
continue;
|
|
35446
|
+
}
|
|
35447
|
+
if (!keepVisible) characters[index] = " ";
|
|
35448
|
+
index += 1;
|
|
35449
|
+
break;
|
|
35450
|
+
}
|
|
35451
|
+
if (!keepVisible && content[index] !== "\n") characters[index] = " ";
|
|
35452
|
+
index += 1;
|
|
35453
|
+
}
|
|
35454
|
+
continue;
|
|
35455
|
+
}
|
|
35456
|
+
if (character === "\"") {
|
|
35457
|
+
index += 1;
|
|
35458
|
+
while (index < end) {
|
|
35459
|
+
if (content[index] === "\"") {
|
|
35460
|
+
if (content[index + 1] === "\"") {
|
|
35461
|
+
index += 2;
|
|
35462
|
+
continue;
|
|
35463
|
+
}
|
|
35464
|
+
index += 1;
|
|
35465
|
+
break;
|
|
35466
|
+
}
|
|
35467
|
+
index += 1;
|
|
35468
|
+
}
|
|
35469
|
+
continue;
|
|
35470
|
+
}
|
|
35471
|
+
if (character === "-" && content[index + 1] === "-") {
|
|
35472
|
+
while (index < end && content[index] !== "\n") {
|
|
35473
|
+
characters[index] = " ";
|
|
35474
|
+
index += 1;
|
|
35475
|
+
}
|
|
35476
|
+
continue;
|
|
35477
|
+
}
|
|
35478
|
+
if (character === "/" && content[index + 1] === "*") {
|
|
35479
|
+
while (index < end) {
|
|
35480
|
+
if (content[index] === "*" && content[index + 1] === "/") {
|
|
35481
|
+
characters[index] = " ";
|
|
35482
|
+
characters[index + 1] = " ";
|
|
35483
|
+
index += 2;
|
|
35484
|
+
break;
|
|
35485
|
+
}
|
|
35486
|
+
if (content[index] !== "\n") characters[index] = " ";
|
|
35487
|
+
index += 1;
|
|
35488
|
+
}
|
|
35489
|
+
continue;
|
|
35490
|
+
}
|
|
35491
|
+
index += 1;
|
|
35492
|
+
}
|
|
35493
|
+
};
|
|
35494
|
+
const sanitizeSqlForScan = (content) => {
|
|
35495
|
+
const characters = content.split("");
|
|
35496
|
+
let index = 0;
|
|
35497
|
+
while (index < content.length) {
|
|
35498
|
+
const character = content[index];
|
|
35499
|
+
if (character === "-" && content[index + 1] === "-") {
|
|
35500
|
+
while (index < content.length && content[index] !== "\n") {
|
|
35501
|
+
characters[index] = " ";
|
|
35502
|
+
index += 1;
|
|
35503
|
+
}
|
|
35504
|
+
continue;
|
|
35505
|
+
}
|
|
35506
|
+
if (character === "/" && content[index + 1] === "*") {
|
|
35507
|
+
while (index < content.length) {
|
|
35508
|
+
if (content[index] === "*" && content[index + 1] === "/") {
|
|
35509
|
+
characters[index] = " ";
|
|
35510
|
+
characters[index + 1] = " ";
|
|
35511
|
+
index += 2;
|
|
35512
|
+
break;
|
|
35513
|
+
}
|
|
35514
|
+
if (content[index] !== "\n") characters[index] = " ";
|
|
35515
|
+
index += 1;
|
|
35516
|
+
}
|
|
35517
|
+
continue;
|
|
35518
|
+
}
|
|
35519
|
+
if (character === "'") {
|
|
35520
|
+
characters[index] = " ";
|
|
35521
|
+
index += 1;
|
|
35522
|
+
while (index < content.length) {
|
|
35523
|
+
if (content[index] === "'") {
|
|
35524
|
+
if (content[index + 1] === "'") {
|
|
35525
|
+
characters[index] = " ";
|
|
35526
|
+
characters[index + 1] = " ";
|
|
35527
|
+
index += 2;
|
|
35528
|
+
continue;
|
|
35529
|
+
}
|
|
35530
|
+
characters[index] = " ";
|
|
35531
|
+
index += 1;
|
|
35532
|
+
break;
|
|
35533
|
+
}
|
|
35534
|
+
if (content[index] !== "\n") characters[index] = " ";
|
|
35535
|
+
index += 1;
|
|
35536
|
+
}
|
|
35537
|
+
continue;
|
|
35538
|
+
}
|
|
35539
|
+
if (character === "$") {
|
|
35540
|
+
const tagMatch = DOLLAR_QUOTE_TAG_PATTERN.exec(content.slice(index));
|
|
35541
|
+
if (tagMatch !== null) {
|
|
35542
|
+
const tag = tagMatch[0];
|
|
35543
|
+
const closeIndex = content.indexOf(tag, index + tag.length);
|
|
35544
|
+
const endIndex = closeIndex < 0 ? content.length : closeIndex + tag.length;
|
|
35545
|
+
const keyword = precedingKeyword(content, index);
|
|
35546
|
+
if (CODE_BODY_KEYWORDS.has(keyword)) blankCodeBodyInterior(content, characters, index + tag.length, endIndex);
|
|
35547
|
+
else for (let blankIndex = index; blankIndex < endIndex; blankIndex += 1) if (content[blankIndex] !== "\n") characters[blankIndex] = " ";
|
|
35548
|
+
index = endIndex;
|
|
35549
|
+
continue;
|
|
35550
|
+
}
|
|
35551
|
+
}
|
|
35552
|
+
if (character === "\"") {
|
|
35553
|
+
index += 1;
|
|
35554
|
+
while (index < content.length) {
|
|
35555
|
+
if (content[index] === "\"") {
|
|
35556
|
+
if (content[index + 1] === "\"") {
|
|
35557
|
+
index += 2;
|
|
35558
|
+
continue;
|
|
35559
|
+
}
|
|
35560
|
+
index += 1;
|
|
35561
|
+
break;
|
|
35562
|
+
}
|
|
35563
|
+
index += 1;
|
|
35564
|
+
}
|
|
35565
|
+
continue;
|
|
35566
|
+
}
|
|
35567
|
+
index += 1;
|
|
35568
|
+
}
|
|
35569
|
+
return characters.join("");
|
|
35570
|
+
};
|
|
35571
|
+
//#endregion
|
|
35572
|
+
//#region src/plugin/rules/security-scan/supabase-table-missing-rls.ts
|
|
35573
|
+
const CREATE_PUBLIC_TABLE_PATTERN = /create\s+(?:unlogged\s+)?table\s+(?:if\s+not\s+exists\s+)?(?!(?:auth|storage|realtime|vault|extensions|graphql|graphql_public|pgbouncer|net|supabase_functions|supabase_migrations|cron|pgsodium|pgmq|information_schema|pg_catalog|pg_temp|private|internal)\s*\.)(?:public\s*\.\s*)?["`]?([A-Za-z_][\w$]*)["`]?(?:\s*\(|\s+as\b)/gi;
|
|
35574
|
+
const enableRlsForTablePattern = (tableName) => new RegExp(`alter\\s+table\\s+(?:if\\s+exists\\s+)?(?:only\\s+)?(?:public\\s*\\.\\s*)?["\`]?${escapeRegExp(tableName)}["\`]?\\s+(?:force\\s+)?enable\\s+row\\s+level\\s+security`, "i");
|
|
35575
|
+
const supabaseTableMissingRls = defineRule({
|
|
35576
|
+
id: "supabase-table-missing-rls",
|
|
35577
|
+
title: "Supabase table created without Row Level Security",
|
|
35578
|
+
severity: "error",
|
|
35579
|
+
recommendation: "Enable RLS in the same migration (`alter table <name> enable row level security;`) and add `auth.uid()`-scoped policies for select/insert/update/delete. A public table without RLS is fully readable and writable with the public anon key.",
|
|
35580
|
+
scan: (file) => {
|
|
35581
|
+
if (!isSupabaseMigrationPath(file.relativePath)) return [];
|
|
35582
|
+
const content = sanitizeSqlForScan(file.content);
|
|
35583
|
+
if (!/create\s+(?:unlogged\s+)?table/i.test(content)) return [];
|
|
35584
|
+
const findings = [];
|
|
35585
|
+
CREATE_PUBLIC_TABLE_PATTERN.lastIndex = 0;
|
|
35586
|
+
for (let match = CREATE_PUBLIC_TABLE_PATTERN.exec(content); match !== null; match = CREATE_PUBLIC_TABLE_PATTERN.exec(content)) {
|
|
35587
|
+
const tableName = match[1];
|
|
35588
|
+
if (tableName === void 0) continue;
|
|
35589
|
+
if (enableRlsForTablePattern(tableName).test(content.slice(match.index))) continue;
|
|
35590
|
+
const location = getLocationAtIndex(content, match.index);
|
|
35591
|
+
findings.push({
|
|
35592
|
+
message: "Supabase migration creates a public table but never enables Row Level Security, leaving every row exposed to the anon key.",
|
|
35593
|
+
line: location.line,
|
|
35594
|
+
column: location.column
|
|
35595
|
+
});
|
|
35596
|
+
}
|
|
35597
|
+
return findings;
|
|
35598
|
+
}
|
|
35599
|
+
});
|
|
35600
|
+
//#endregion
|
|
35161
35601
|
//#region src/plugin/rules/security-scan/svg-filter-clickjacking-risk.ts
|
|
35162
35602
|
const svgFilterClickjackingRisk = defineRule({
|
|
35163
35603
|
id: "svg-filter-clickjacking-risk",
|
|
@@ -35856,6 +36296,47 @@ const tenantStaticProxyRisk = defineRule({
|
|
|
35856
36296
|
})
|
|
35857
36297
|
});
|
|
35858
36298
|
//#endregion
|
|
36299
|
+
//#region src/plugin/rules/security-scan/unsafe-json-in-html.ts
|
|
36300
|
+
const SINK_JSON_STRINGIFY_PATTERNS = [/dangerouslySetInnerHTML\s*=\s*\{\{\s*__html\s*:[\s\S]{0,300}?\bJSON\.stringify\s*\(/gi, /<script\b[^>]*>(?:(?!<\/script>)[\s\S]){0,300}?\bJSON\.stringify\s*\(/gi];
|
|
36301
|
+
const RETURN_ESCAPE_PATTERN = /^[\s)]*\.replace\s*\([^)]*(?:\\u003[cC]|<|<)/;
|
|
36302
|
+
const ESCAPE_WRAPPER_PATTERN = /(?:\b(?:escapeHtml|escapeJSON|escapeJson|htmlEscape|jsesc)|(?<![.\w])(?:serialize|serializeJavascript|devalue|uneval|superjson))\s*\(\s*$/i;
|
|
36303
|
+
const JSON_STRINGIFY_TOKEN_PATTERN = /\bJSON\.stringify\s*\($/i;
|
|
36304
|
+
const RETURN_LOOKAHEAD_CHARS = 160;
|
|
36305
|
+
const unsafeJsonInHtml = defineRule({
|
|
36306
|
+
id: "unsafe-json-in-html",
|
|
36307
|
+
title: "Unescaped JSON in HTML or script sink",
|
|
36308
|
+
severity: "warn",
|
|
36309
|
+
recommendation: "JSON.stringify does not HTML-escape, so a `<\/script>` (or `<`) in the data breaks out and becomes XSS. Use an HTML-safe serializer (serialize-javascript, devalue) or escape `<`, `>`, and `&`, or pass data via a JSON `<script type=\"application/json\">` read with JSON.parse.",
|
|
36310
|
+
scan: (file) => {
|
|
36311
|
+
if (!isProductionSourcePath(file.relativePath)) return [];
|
|
36312
|
+
const content = getScannableContent(file);
|
|
36313
|
+
if (!content.includes("JSON.stringify")) return [];
|
|
36314
|
+
const findings = [];
|
|
36315
|
+
const seenIndices = /* @__PURE__ */ new Set();
|
|
36316
|
+
for (const pattern of SINK_JSON_STRINGIFY_PATTERNS) {
|
|
36317
|
+
pattern.lastIndex = 0;
|
|
36318
|
+
for (let match = pattern.exec(content); match !== null; match = pattern.exec(content)) {
|
|
36319
|
+
const beforeStringify = match[0].replace(JSON_STRINGIFY_TOKEN_PATTERN, "");
|
|
36320
|
+
if (ESCAPE_WRAPPER_PATTERN.test(beforeStringify)) continue;
|
|
36321
|
+
const closeParenIndex = findMatchingBracket(content, match.index + match[0].length - 1);
|
|
36322
|
+
if (closeParenIndex >= 0) {
|
|
36323
|
+
const afterReturn = content.slice(closeParenIndex + 1, closeParenIndex + 1 + RETURN_LOOKAHEAD_CHARS);
|
|
36324
|
+
if (RETURN_ESCAPE_PATTERN.test(afterReturn)) continue;
|
|
36325
|
+
}
|
|
36326
|
+
if (seenIndices.has(match.index)) continue;
|
|
36327
|
+
seenIndices.add(match.index);
|
|
36328
|
+
const location = getLocationAtIndex(content, match.index);
|
|
36329
|
+
findings.push({
|
|
36330
|
+
message: "JSON.stringify is embedded in HTML/script markup without HTML-escaping; data containing `<\/script>` or `<` breaks out and becomes XSS.",
|
|
36331
|
+
line: location.line,
|
|
36332
|
+
column: location.column
|
|
36333
|
+
});
|
|
36334
|
+
}
|
|
36335
|
+
}
|
|
36336
|
+
return findings;
|
|
36337
|
+
}
|
|
36338
|
+
});
|
|
36339
|
+
//#endregion
|
|
35859
36340
|
//#region src/plugin/rules/security-scan/untrusted-redirect-following.ts
|
|
35860
36341
|
const OUTBOUND_FETCH_CALL_PATTERN = /(?:(?<![.\w$])fetch|\baxios\.\s*(?:get|post|put|delete|head)|\bgot|\bgot\.\s*(?:get|post))\s*\(\s*([^,)]+)/;
|
|
35861
36342
|
const CALLER_STYLE_URL_NAME_PATTERN = /\b(?:url|targetUrl|callbackUrl|redirectUrl|webhookUrl|companyUrl|websiteUrl|domainUrl|imageUrl|fetchUrl|next|return_to|returnTo|destination|location)\b/i;
|
|
@@ -36003,7 +36484,7 @@ const voidDomElementsNoChildren = defineRule({
|
|
|
36003
36484
|
//#region src/plugin/rules/security-scan/webhook-signature-risk.ts
|
|
36004
36485
|
const WEBHOOK_HANDLER_PATTERN = /(?:^|\/)[^/]*webhook[^/]*\/|(?:^|\/)[^/]*webhook[^/]*\.[cm]?[jt]s$|\bwebhook\b/i;
|
|
36005
36486
|
const WEBHOOK_ENTRYPOINT_PATTERN = /\b(?:export\s+(?:async\s+)?function\s+POST|export\s+const\s+(?:POST|handler|webhook)|webhookHandler|webhookRoute)\b/i;
|
|
36006
|
-
const WEBHOOK_SIGNATURE_VERIFICATION_PATTERN = /verifySignature|verify.*signature|verify\w*(?:Webhook|Auth)|constructEvent|createHmac|timingSafeEqual|svix|webhookSecret|stripe\.webhooks|["'][\w-]*signature["']
|
|
36487
|
+
const WEBHOOK_SIGNATURE_VERIFICATION_PATTERN = new RegExp(`${/verifySignature|verify.*signature|verify\w*(?:Webhook|Auth)|constructEvent|createHmac|timingSafeEqual|svix|webhookSecret|stripe\.webhooks|["'][\w-]*signature["']/.source}|${/\b[A-Za-z]{0,40}(?:verif|valid|check|assert|authenticat|compare|guard)[A-Za-z]{0,40}(?:secret|signature|hmac|webhook|digest)[A-Za-z]{0,40}\s*\(/.source}`, "i");
|
|
36007
36488
|
const OUTBOUND_WEBHOOK_URL_MENTION_PATTERN = /webhook[\s_-]?ur[il]\w*/gi;
|
|
36008
36489
|
const OUTBOUND_WEBHOOK_CONFIG_PATTERN = /process\.env\.\w*WEBHOOK_URL|\b(?:send|post|dispatch|publish|notify)\w*Webhook/;
|
|
36009
36490
|
const REQUEST_READ_PATTERN = /\b(?:req|request)\b/;
|
|
@@ -37164,6 +37645,18 @@ const reactDoctorRules = [
|
|
|
37164
37645
|
tags: [...new Set(["security-scan", ...insecureCryptoRisk.tags ?? []])]
|
|
37165
37646
|
}
|
|
37166
37647
|
},
|
|
37648
|
+
{
|
|
37649
|
+
key: "react-doctor/insecure-session-cookie",
|
|
37650
|
+
id: "insecure-session-cookie",
|
|
37651
|
+
source: "react-doctor",
|
|
37652
|
+
originallyExternal: false,
|
|
37653
|
+
rule: {
|
|
37654
|
+
...insecureSessionCookie,
|
|
37655
|
+
framework: "global",
|
|
37656
|
+
category: "Security",
|
|
37657
|
+
tags: [...new Set(["security-scan", ...insecureSessionCookie.tags ?? []])]
|
|
37658
|
+
}
|
|
37659
|
+
},
|
|
37167
37660
|
{
|
|
37168
37661
|
key: "react-doctor/interactive-supports-focus",
|
|
37169
37662
|
id: "interactive-supports-focus",
|
|
@@ -37606,6 +38099,18 @@ const reactDoctorRules = [
|
|
|
37606
38099
|
requires: [...new Set(["react", ...jsxPropsNoSpreading.requires ?? []])]
|
|
37607
38100
|
}
|
|
37608
38101
|
},
|
|
38102
|
+
{
|
|
38103
|
+
key: "react-doctor/jwt-insecure-verification",
|
|
38104
|
+
id: "jwt-insecure-verification",
|
|
38105
|
+
source: "react-doctor",
|
|
38106
|
+
originallyExternal: false,
|
|
38107
|
+
rule: {
|
|
38108
|
+
...jwtInsecureVerification,
|
|
38109
|
+
framework: "global",
|
|
38110
|
+
category: "Security",
|
|
38111
|
+
tags: [...new Set(["security-scan", ...jwtInsecureVerification.tags ?? []])]
|
|
38112
|
+
}
|
|
38113
|
+
},
|
|
37609
38114
|
{
|
|
37610
38115
|
key: "react-doctor/key-lifecycle-risk",
|
|
37611
38116
|
id: "key-lifecycle-risk",
|
|
@@ -39756,6 +40261,18 @@ const reactDoctorRules = [
|
|
|
39756
40261
|
tags: [...new Set(["security-scan", ...repositorySecretFile.tags ?? []])]
|
|
39757
40262
|
}
|
|
39758
40263
|
},
|
|
40264
|
+
{
|
|
40265
|
+
key: "react-doctor/request-body-mass-assignment",
|
|
40266
|
+
id: "request-body-mass-assignment",
|
|
40267
|
+
source: "react-doctor",
|
|
40268
|
+
originallyExternal: false,
|
|
40269
|
+
rule: {
|
|
40270
|
+
...requestBodyMassAssignment,
|
|
40271
|
+
framework: "global",
|
|
40272
|
+
category: "Security",
|
|
40273
|
+
tags: [...new Set(["security-scan", ...requestBodyMassAssignment.tags ?? []])]
|
|
40274
|
+
}
|
|
40275
|
+
},
|
|
39759
40276
|
{
|
|
39760
40277
|
key: "react-doctor/require-render-return",
|
|
39761
40278
|
id: "require-render-return",
|
|
@@ -40344,6 +40861,18 @@ const reactDoctorRules = [
|
|
|
40344
40861
|
requires: [...new Set(["react", ...scope.requires ?? []])]
|
|
40345
40862
|
}
|
|
40346
40863
|
},
|
|
40864
|
+
{
|
|
40865
|
+
key: "react-doctor/secret-in-fallback",
|
|
40866
|
+
id: "secret-in-fallback",
|
|
40867
|
+
source: "react-doctor",
|
|
40868
|
+
originallyExternal: false,
|
|
40869
|
+
rule: {
|
|
40870
|
+
...secretInFallback,
|
|
40871
|
+
framework: "global",
|
|
40872
|
+
category: "Security",
|
|
40873
|
+
tags: [...new Set(["security-scan", ...secretInFallback.tags ?? []])]
|
|
40874
|
+
}
|
|
40875
|
+
},
|
|
40347
40876
|
{
|
|
40348
40877
|
key: "react-doctor/self-closing-comp",
|
|
40349
40878
|
id: "self-closing-comp",
|
|
@@ -40500,6 +41029,18 @@ const reactDoctorRules = [
|
|
|
40500
41029
|
tags: [...new Set(["security-scan", ...supabaseRlsPolicyRisk.tags ?? []])]
|
|
40501
41030
|
}
|
|
40502
41031
|
},
|
|
41032
|
+
{
|
|
41033
|
+
key: "react-doctor/supabase-table-missing-rls",
|
|
41034
|
+
id: "supabase-table-missing-rls",
|
|
41035
|
+
source: "react-doctor",
|
|
41036
|
+
originallyExternal: false,
|
|
41037
|
+
rule: {
|
|
41038
|
+
...supabaseTableMissingRls,
|
|
41039
|
+
framework: "global",
|
|
41040
|
+
category: "Security",
|
|
41041
|
+
tags: [...new Set(["security-scan", ...supabaseTableMissingRls.tags ?? []])]
|
|
41042
|
+
}
|
|
41043
|
+
},
|
|
40503
41044
|
{
|
|
40504
41045
|
key: "react-doctor/svg-filter-clickjacking-risk",
|
|
40505
41046
|
id: "svg-filter-clickjacking-risk",
|
|
@@ -40690,6 +41231,18 @@ const reactDoctorRules = [
|
|
|
40690
41231
|
tags: [...new Set(["security-scan", ...tenantStaticProxyRisk.tags ?? []])]
|
|
40691
41232
|
}
|
|
40692
41233
|
},
|
|
41234
|
+
{
|
|
41235
|
+
key: "react-doctor/unsafe-json-in-html",
|
|
41236
|
+
id: "unsafe-json-in-html",
|
|
41237
|
+
source: "react-doctor",
|
|
41238
|
+
originallyExternal: false,
|
|
41239
|
+
rule: {
|
|
41240
|
+
...unsafeJsonInHtml,
|
|
41241
|
+
framework: "global",
|
|
41242
|
+
category: "Security",
|
|
41243
|
+
tags: [...new Set(["security-scan", ...unsafeJsonInHtml.tags ?? []])]
|
|
41244
|
+
}
|
|
41245
|
+
},
|
|
40693
41246
|
{
|
|
40694
41247
|
key: "react-doctor/untrusted-redirect-following",
|
|
40695
41248
|
id: "untrusted-redirect-following",
|