oxlint-plugin-react-doctor 0.5.5-dev.5fc0e27 → 0.5.5-dev.cf9e05b
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 +252 -0
- package/dist/index.js +534 -1
- package/package.json +1 -1
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") ||
|
|
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]|<|<)/;
|
|
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.
|
|
3
|
+
"version": "0.5.5-dev.cf9e05b",
|
|
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",
|