oxlint-plugin-react-doctor 0.5.5-dev.bac7c82 → 0.5.5-dev.ea3b827

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 CHANGED
@@ -1440,6 +1440,27 @@ declare const REACT_DOCTOR_RULES: readonly [{
1440
1440
  readonly recommendation?: string;
1441
1441
  readonly create: (context: RuleContext) => RuleVisitors;
1442
1442
  };
1443
+ }, {
1444
+ readonly key: "react-doctor/insecure-session-cookie";
1445
+ readonly id: "insecure-session-cookie";
1446
+ readonly source: "react-doctor";
1447
+ readonly originallyExternal: false;
1448
+ readonly rule: {
1449
+ readonly framework: "global";
1450
+ readonly category: "Security";
1451
+ readonly tags: readonly string[];
1452
+ readonly id: string;
1453
+ readonly title?: string;
1454
+ readonly severity: RuleSeverity;
1455
+ readonly requires?: ReadonlyArray<string>;
1456
+ readonly disabledBy?: ReadonlyArray<string>;
1457
+ readonly defaultEnabled?: boolean;
1458
+ readonly lifecycle?: "retired";
1459
+ readonly scan?: FileScan;
1460
+ readonly committedFilesOnly?: boolean;
1461
+ readonly recommendation?: string;
1462
+ readonly create: (context: RuleContext) => RuleVisitors;
1463
+ };
1443
1464
  }, {
1444
1465
  readonly key: "react-doctor/interactive-supports-focus";
1445
1466
  readonly id: "interactive-supports-focus";
@@ -2238,6 +2259,27 @@ declare const REACT_DOCTOR_RULES: readonly [{
2238
2259
  readonly recommendation?: string;
2239
2260
  readonly create: (context: RuleContext) => RuleVisitors;
2240
2261
  };
2262
+ }, {
2263
+ readonly key: "react-doctor/jwt-insecure-verification";
2264
+ readonly id: "jwt-insecure-verification";
2265
+ readonly source: "react-doctor";
2266
+ readonly originallyExternal: false;
2267
+ readonly rule: {
2268
+ readonly framework: "global";
2269
+ readonly category: "Security";
2270
+ readonly tags: readonly string[];
2271
+ readonly id: string;
2272
+ readonly title?: string;
2273
+ readonly severity: RuleSeverity;
2274
+ readonly requires?: ReadonlyArray<string>;
2275
+ readonly disabledBy?: ReadonlyArray<string>;
2276
+ readonly defaultEnabled?: boolean;
2277
+ readonly lifecycle?: "retired";
2278
+ readonly scan?: FileScan;
2279
+ readonly committedFilesOnly?: boolean;
2280
+ readonly recommendation?: string;
2281
+ readonly create: (context: RuleContext) => RuleVisitors;
2282
+ };
2241
2283
  }, {
2242
2284
  readonly key: "react-doctor/key-lifecycle-risk";
2243
2285
  readonly id: "key-lifecycle-risk";
@@ -6144,6 +6186,27 @@ declare const REACT_DOCTOR_RULES: readonly [{
6144
6186
  readonly recommendation?: string;
6145
6187
  readonly create: (context: RuleContext) => RuleVisitors;
6146
6188
  };
6189
+ }, {
6190
+ readonly key: "react-doctor/request-body-mass-assignment";
6191
+ readonly id: "request-body-mass-assignment";
6192
+ readonly source: "react-doctor";
6193
+ readonly originallyExternal: false;
6194
+ readonly rule: {
6195
+ readonly framework: "global";
6196
+ readonly category: "Security";
6197
+ readonly tags: readonly string[];
6198
+ readonly id: string;
6199
+ readonly title?: string;
6200
+ readonly severity: RuleSeverity;
6201
+ readonly requires?: ReadonlyArray<string>;
6202
+ readonly disabledBy?: ReadonlyArray<string>;
6203
+ readonly defaultEnabled?: boolean;
6204
+ readonly lifecycle?: "retired";
6205
+ readonly scan?: FileScan;
6206
+ readonly committedFilesOnly?: boolean;
6207
+ readonly recommendation?: string;
6208
+ readonly create: (context: RuleContext) => RuleVisitors;
6209
+ };
6147
6210
  }, {
6148
6211
  readonly key: "react-doctor/require-render-return";
6149
6212
  readonly id: "require-render-return";
@@ -7173,6 +7236,27 @@ declare const REACT_DOCTOR_RULES: readonly [{
7173
7236
  readonly recommendation?: string;
7174
7237
  readonly create: (context: RuleContext) => RuleVisitors;
7175
7238
  };
7239
+ }, {
7240
+ readonly key: "react-doctor/secret-in-fallback";
7241
+ readonly id: "secret-in-fallback";
7242
+ readonly source: "react-doctor";
7243
+ readonly originallyExternal: false;
7244
+ readonly rule: {
7245
+ readonly framework: "global";
7246
+ readonly category: "Security";
7247
+ readonly tags: readonly string[];
7248
+ readonly id: string;
7249
+ readonly title?: string;
7250
+ readonly severity: RuleSeverity;
7251
+ readonly requires?: ReadonlyArray<string>;
7252
+ readonly disabledBy?: ReadonlyArray<string>;
7253
+ readonly defaultEnabled?: boolean;
7254
+ readonly lifecycle?: "retired";
7255
+ readonly scan?: FileScan;
7256
+ readonly committedFilesOnly?: boolean;
7257
+ readonly recommendation?: string;
7258
+ readonly create: (context: RuleContext) => RuleVisitors;
7259
+ };
7176
7260
  }, {
7177
7261
  readonly key: "react-doctor/self-closing-comp";
7178
7262
  readonly id: "self-closing-comp";
@@ -7446,6 +7530,27 @@ declare const REACT_DOCTOR_RULES: readonly [{
7446
7530
  readonly recommendation?: string;
7447
7531
  readonly create: (context: RuleContext) => RuleVisitors;
7448
7532
  };
7533
+ }, {
7534
+ readonly key: "react-doctor/supabase-table-missing-rls";
7535
+ readonly id: "supabase-table-missing-rls";
7536
+ readonly source: "react-doctor";
7537
+ readonly originallyExternal: false;
7538
+ readonly rule: {
7539
+ readonly framework: "global";
7540
+ readonly category: "Security";
7541
+ readonly tags: readonly string[];
7542
+ readonly id: string;
7543
+ readonly title?: string;
7544
+ readonly severity: RuleSeverity;
7545
+ readonly requires?: ReadonlyArray<string>;
7546
+ readonly disabledBy?: ReadonlyArray<string>;
7547
+ readonly defaultEnabled?: boolean;
7548
+ readonly lifecycle?: "retired";
7549
+ readonly scan?: FileScan;
7550
+ readonly committedFilesOnly?: boolean;
7551
+ readonly recommendation?: string;
7552
+ readonly create: (context: RuleContext) => RuleVisitors;
7553
+ };
7449
7554
  }, {
7450
7555
  readonly key: "react-doctor/svg-filter-clickjacking-risk";
7451
7556
  readonly id: "svg-filter-clickjacking-risk";
@@ -7803,6 +7908,27 @@ declare const REACT_DOCTOR_RULES: readonly [{
7803
7908
  readonly recommendation?: string;
7804
7909
  readonly create: (context: RuleContext) => RuleVisitors;
7805
7910
  };
7911
+ }, {
7912
+ readonly key: "react-doctor/unsafe-json-in-html";
7913
+ readonly id: "unsafe-json-in-html";
7914
+ readonly source: "react-doctor";
7915
+ readonly originallyExternal: false;
7916
+ readonly rule: {
7917
+ readonly framework: "global";
7918
+ readonly category: "Security";
7919
+ readonly tags: readonly string[];
7920
+ readonly id: string;
7921
+ readonly title?: string;
7922
+ readonly severity: RuleSeverity;
7923
+ readonly requires?: ReadonlyArray<string>;
7924
+ readonly disabledBy?: ReadonlyArray<string>;
7925
+ readonly defaultEnabled?: boolean;
7926
+ readonly lifecycle?: "retired";
7927
+ readonly scan?: FileScan;
7928
+ readonly committedFilesOnly?: boolean;
7929
+ readonly recommendation?: string;
7930
+ readonly create: (context: RuleContext) => RuleVisitors;
7931
+ };
7806
7932
  }, {
7807
7933
  readonly key: "react-doctor/untrusted-redirect-following";
7808
7934
  readonly id: "untrusted-redirect-following";
@@ -9339,6 +9465,27 @@ declare const RULES: readonly [{
9339
9465
  readonly recommendation?: string;
9340
9466
  readonly create: (context: RuleContext) => RuleVisitors;
9341
9467
  };
9468
+ }, {
9469
+ readonly key: "react-doctor/insecure-session-cookie";
9470
+ readonly id: "insecure-session-cookie";
9471
+ readonly source: "react-doctor";
9472
+ readonly originallyExternal: false;
9473
+ readonly rule: {
9474
+ readonly framework: "global";
9475
+ readonly category: "Security";
9476
+ readonly tags: readonly string[];
9477
+ readonly id: string;
9478
+ readonly title?: string;
9479
+ readonly severity: RuleSeverity;
9480
+ readonly requires?: ReadonlyArray<string>;
9481
+ readonly disabledBy?: ReadonlyArray<string>;
9482
+ readonly defaultEnabled?: boolean;
9483
+ readonly lifecycle?: "retired";
9484
+ readonly scan?: FileScan;
9485
+ readonly committedFilesOnly?: boolean;
9486
+ readonly recommendation?: string;
9487
+ readonly create: (context: RuleContext) => RuleVisitors;
9488
+ };
9342
9489
  }, {
9343
9490
  readonly key: "react-doctor/interactive-supports-focus";
9344
9491
  readonly id: "interactive-supports-focus";
@@ -10137,6 +10284,27 @@ declare const RULES: readonly [{
10137
10284
  readonly recommendation?: string;
10138
10285
  readonly create: (context: RuleContext) => RuleVisitors;
10139
10286
  };
10287
+ }, {
10288
+ readonly key: "react-doctor/jwt-insecure-verification";
10289
+ readonly id: "jwt-insecure-verification";
10290
+ readonly source: "react-doctor";
10291
+ readonly originallyExternal: false;
10292
+ readonly rule: {
10293
+ readonly framework: "global";
10294
+ readonly category: "Security";
10295
+ readonly tags: readonly string[];
10296
+ readonly id: string;
10297
+ readonly title?: string;
10298
+ readonly severity: RuleSeverity;
10299
+ readonly requires?: ReadonlyArray<string>;
10300
+ readonly disabledBy?: ReadonlyArray<string>;
10301
+ readonly defaultEnabled?: boolean;
10302
+ readonly lifecycle?: "retired";
10303
+ readonly scan?: FileScan;
10304
+ readonly committedFilesOnly?: boolean;
10305
+ readonly recommendation?: string;
10306
+ readonly create: (context: RuleContext) => RuleVisitors;
10307
+ };
10140
10308
  }, {
10141
10309
  readonly key: "react-doctor/key-lifecycle-risk";
10142
10310
  readonly id: "key-lifecycle-risk";
@@ -14043,6 +14211,27 @@ declare const RULES: readonly [{
14043
14211
  readonly recommendation?: string;
14044
14212
  readonly create: (context: RuleContext) => RuleVisitors;
14045
14213
  };
14214
+ }, {
14215
+ readonly key: "react-doctor/request-body-mass-assignment";
14216
+ readonly id: "request-body-mass-assignment";
14217
+ readonly source: "react-doctor";
14218
+ readonly originallyExternal: false;
14219
+ readonly rule: {
14220
+ readonly framework: "global";
14221
+ readonly category: "Security";
14222
+ readonly tags: readonly string[];
14223
+ readonly id: string;
14224
+ readonly title?: string;
14225
+ readonly severity: RuleSeverity;
14226
+ readonly requires?: ReadonlyArray<string>;
14227
+ readonly disabledBy?: ReadonlyArray<string>;
14228
+ readonly defaultEnabled?: boolean;
14229
+ readonly lifecycle?: "retired";
14230
+ readonly scan?: FileScan;
14231
+ readonly committedFilesOnly?: boolean;
14232
+ readonly recommendation?: string;
14233
+ readonly create: (context: RuleContext) => RuleVisitors;
14234
+ };
14046
14235
  }, {
14047
14236
  readonly key: "react-doctor/require-render-return";
14048
14237
  readonly id: "require-render-return";
@@ -15072,6 +15261,27 @@ declare const RULES: readonly [{
15072
15261
  readonly recommendation?: string;
15073
15262
  readonly create: (context: RuleContext) => RuleVisitors;
15074
15263
  };
15264
+ }, {
15265
+ readonly key: "react-doctor/secret-in-fallback";
15266
+ readonly id: "secret-in-fallback";
15267
+ readonly source: "react-doctor";
15268
+ readonly originallyExternal: false;
15269
+ readonly rule: {
15270
+ readonly framework: "global";
15271
+ readonly category: "Security";
15272
+ readonly tags: readonly string[];
15273
+ readonly id: string;
15274
+ readonly title?: string;
15275
+ readonly severity: RuleSeverity;
15276
+ readonly requires?: ReadonlyArray<string>;
15277
+ readonly disabledBy?: ReadonlyArray<string>;
15278
+ readonly defaultEnabled?: boolean;
15279
+ readonly lifecycle?: "retired";
15280
+ readonly scan?: FileScan;
15281
+ readonly committedFilesOnly?: boolean;
15282
+ readonly recommendation?: string;
15283
+ readonly create: (context: RuleContext) => RuleVisitors;
15284
+ };
15075
15285
  }, {
15076
15286
  readonly key: "react-doctor/self-closing-comp";
15077
15287
  readonly id: "self-closing-comp";
@@ -15345,6 +15555,27 @@ declare const RULES: readonly [{
15345
15555
  readonly recommendation?: string;
15346
15556
  readonly create: (context: RuleContext) => RuleVisitors;
15347
15557
  };
15558
+ }, {
15559
+ readonly key: "react-doctor/supabase-table-missing-rls";
15560
+ readonly id: "supabase-table-missing-rls";
15561
+ readonly source: "react-doctor";
15562
+ readonly originallyExternal: false;
15563
+ readonly rule: {
15564
+ readonly framework: "global";
15565
+ readonly category: "Security";
15566
+ readonly tags: readonly string[];
15567
+ readonly id: string;
15568
+ readonly title?: string;
15569
+ readonly severity: RuleSeverity;
15570
+ readonly requires?: ReadonlyArray<string>;
15571
+ readonly disabledBy?: ReadonlyArray<string>;
15572
+ readonly defaultEnabled?: boolean;
15573
+ readonly lifecycle?: "retired";
15574
+ readonly scan?: FileScan;
15575
+ readonly committedFilesOnly?: boolean;
15576
+ readonly recommendation?: string;
15577
+ readonly create: (context: RuleContext) => RuleVisitors;
15578
+ };
15348
15579
  }, {
15349
15580
  readonly key: "react-doctor/svg-filter-clickjacking-risk";
15350
15581
  readonly id: "svg-filter-clickjacking-risk";
@@ -15702,6 +15933,27 @@ declare const RULES: readonly [{
15702
15933
  readonly recommendation?: string;
15703
15934
  readonly create: (context: RuleContext) => RuleVisitors;
15704
15935
  };
15936
+ }, {
15937
+ readonly key: "react-doctor/unsafe-json-in-html";
15938
+ readonly id: "unsafe-json-in-html";
15939
+ readonly source: "react-doctor";
15940
+ readonly originallyExternal: false;
15941
+ readonly rule: {
15942
+ readonly framework: "global";
15943
+ readonly category: "Security";
15944
+ readonly tags: readonly string[];
15945
+ readonly id: string;
15946
+ readonly title?: string;
15947
+ readonly severity: RuleSeverity;
15948
+ readonly requires?: ReadonlyArray<string>;
15949
+ readonly disabledBy?: ReadonlyArray<string>;
15950
+ readonly defaultEnabled?: boolean;
15951
+ readonly lifecycle?: "retired";
15952
+ readonly scan?: FileScan;
15953
+ readonly committedFilesOnly?: boolean;
15954
+ readonly recommendation?: string;
15955
+ readonly create: (context: RuleContext) => RuleVisitors;
15956
+ };
15705
15957
  }, {
15706
15958
  readonly key: "react-doctor/untrusted-redirect-following";
15707
15959
  readonly id: "untrusted-redirect-following";
package/dist/index.js CHANGED
@@ -8509,6 +8509,136 @@ const insecureCryptoRisk = defineRule({
8509
8509
  }
8510
8510
  });
8511
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
8512
8642
  //#region src/plugin/constants/event-handlers.ts
8513
8643
  const MOUSE_EVENT_HANDLERS = [
8514
8644
  "onClick",
@@ -12714,6 +12844,64 @@ const jsxPropsNoSpreading = defineRule({
12714
12844
  }
12715
12845
  });
12716
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
12717
12905
  //#region src/plugin/rules/security-scan/key-lifecycle-risk.ts
12718
12906
  const keyLifecycleRisk = defineRule({
12719
12907
  id: "key-lifecycle-risk",
@@ -27925,6 +28113,20 @@ const repositorySecretFile = defineRule({
27925
28113
  }
27926
28114
  });
27927
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
27928
28130
  //#region src/plugin/utils/function-body-has-return-with-value.ts
27929
28131
  const functionBodyHasReturnWithValue = (functionNode) => {
27930
28132
  if (functionNode.type === "ArrowFunctionExpression" && "body" in functionNode) {
@@ -34350,6 +34552,17 @@ const scope = defineRule({
34350
34552
  });
34351
34553
  } })
34352
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
+ });
34353
34566
  //#endregion
34354
34567
  //#region src/plugin/rules/react-builtins/self-closing-comp.ts
34355
34568
  const MESSAGE$2 = "This tag has no children, so the closing tag adds noise without changing output.";
@@ -35159,8 +35372,11 @@ const supabaseClientOwnedAuthzField = defineRule({
35159
35372
  })
35160
35373
  });
35161
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
35162
35378
  //#region src/plugin/rules/security-scan/utils/is-sql-path.ts
35163
- const isSqlPath = (relativePath) => relativePath.endsWith(".sql") || /(?:^|\/)supabase\/(?:migrations|schemas)\//.test(relativePath);
35379
+ const isSqlPath = (relativePath) => relativePath.endsWith(".sql") || isSupabaseMigrationPath(relativePath);
35164
35380
  const supabaseRlsPolicyRisk = defineRule({
35165
35381
  id: "supabase-rls-policy-risk",
35166
35382
  title: "Permissive Supabase RLS policy",
@@ -35178,6 +35394,210 @@ const supabaseRlsPolicyRisk = defineRule({
35178
35394
  })
35179
35395
  });
35180
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
35181
35601
  //#region src/plugin/rules/security-scan/svg-filter-clickjacking-risk.ts
35182
35602
  const svgFilterClickjackingRisk = defineRule({
35183
35603
  id: "svg-filter-clickjacking-risk",
@@ -35876,6 +36296,47 @@ const tenantStaticProxyRisk = defineRule({
35876
36296
  })
35877
36297
  });
35878
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]|&lt;|<)/;
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
35879
36340
  //#region src/plugin/rules/security-scan/untrusted-redirect-following.ts
35880
36341
  const OUTBOUND_FETCH_CALL_PATTERN = /(?:(?<![.\w$])fetch|\baxios\.\s*(?:get|post|put|delete|head)|\bgot|\bgot\.\s*(?:get|post))\s*\(\s*([^,)]+)/;
35881
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;
@@ -37184,6 +37645,18 @@ const reactDoctorRules = [
37184
37645
  tags: [...new Set(["security-scan", ...insecureCryptoRisk.tags ?? []])]
37185
37646
  }
37186
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
+ },
37187
37660
  {
37188
37661
  key: "react-doctor/interactive-supports-focus",
37189
37662
  id: "interactive-supports-focus",
@@ -37626,6 +38099,18 @@ const reactDoctorRules = [
37626
38099
  requires: [...new Set(["react", ...jsxPropsNoSpreading.requires ?? []])]
37627
38100
  }
37628
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
+ },
37629
38114
  {
37630
38115
  key: "react-doctor/key-lifecycle-risk",
37631
38116
  id: "key-lifecycle-risk",
@@ -39776,6 +40261,18 @@ const reactDoctorRules = [
39776
40261
  tags: [...new Set(["security-scan", ...repositorySecretFile.tags ?? []])]
39777
40262
  }
39778
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
+ },
39779
40276
  {
39780
40277
  key: "react-doctor/require-render-return",
39781
40278
  id: "require-render-return",
@@ -40364,6 +40861,18 @@ const reactDoctorRules = [
40364
40861
  requires: [...new Set(["react", ...scope.requires ?? []])]
40365
40862
  }
40366
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
+ },
40367
40876
  {
40368
40877
  key: "react-doctor/self-closing-comp",
40369
40878
  id: "self-closing-comp",
@@ -40520,6 +41029,18 @@ const reactDoctorRules = [
40520
41029
  tags: [...new Set(["security-scan", ...supabaseRlsPolicyRisk.tags ?? []])]
40521
41030
  }
40522
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
+ },
40523
41044
  {
40524
41045
  key: "react-doctor/svg-filter-clickjacking-risk",
40525
41046
  id: "svg-filter-clickjacking-risk",
@@ -40710,6 +41231,18 @@ const reactDoctorRules = [
40710
41231
  tags: [...new Set(["security-scan", ...tenantStaticProxyRisk.tags ?? []])]
40711
41232
  }
40712
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
+ },
40713
41246
  {
40714
41247
  key: "react-doctor/untrusted-redirect-following",
40715
41248
  id: "untrusted-redirect-following",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "oxlint-plugin-react-doctor",
3
- "version": "0.5.5-dev.bac7c82",
3
+ "version": "0.5.5-dev.ea3b827",
4
4
  "description": "oxlint plugin for React Doctor: diagnose React codebases for security, performance, correctness, accessibility, bundle-size, and architecture issues",
5
5
  "keywords": [
6
6
  "accessibility",