xploitscan-shared-rules 1.6.2 → 1.7.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.cjs +90 -26
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +90 -26
- package/dist/index.js.map +1 -1
- package/package.json +5 -5
package/dist/index.cjs
CHANGED
|
@@ -887,6 +887,17 @@ function isInsideFixMessage(content, matchIndex) {
|
|
|
887
887
|
const lineText = content.substring(lineStart, content.indexOf("\n", matchIndex));
|
|
888
888
|
return /(?:fix|description|message|suggestion|hint|help|example|doc|comment)\s*[:=(]/i.test(lineText) || /return\s*["'`].*\b(?:Use|Replace|Add|Move|Set|Enable|Disable|Never|Don't|Do not|Instead)\b/i.test(lineText);
|
|
889
889
|
}
|
|
890
|
+
function isInlineSilenced(content, matchIndex, ruleId) {
|
|
891
|
+
const lines = content.split("\n");
|
|
892
|
+
const matchLineNum = content.substring(0, matchIndex).split("\n").length - 1;
|
|
893
|
+
const matchLine = lines[matchLineNum] ?? "";
|
|
894
|
+
const prevLine = matchLineNum > 0 ? lines[matchLineNum - 1] ?? "" : "";
|
|
895
|
+
const marker = new RegExp(
|
|
896
|
+
`(?://|/\\*|#)\\s*(?:${ruleId}|scanner)-OK\\b`,
|
|
897
|
+
"i"
|
|
898
|
+
);
|
|
899
|
+
return marker.test(matchLine) || marker.test(prevLine);
|
|
900
|
+
}
|
|
890
901
|
function findMatches(content, pattern, rule, filePath, fixTemplate) {
|
|
891
902
|
const matches = [];
|
|
892
903
|
const lines = content.split("\n");
|
|
@@ -1015,6 +1026,16 @@ var hardcodedSecrets = {
|
|
|
1015
1026
|
const secretMatch = lineText.match(/[:=]\s*["'`]([^"'`]*)["'`]/);
|
|
1016
1027
|
if (secretMatch && secretMatch[1].length < floor) continue;
|
|
1017
1028
|
}
|
|
1029
|
+
if (pi === 6) {
|
|
1030
|
+
const valMatch = lineText.match(/[:=]\s*["'`]([^"'`]+)["'`]/);
|
|
1031
|
+
const nameMatch = lineText.match(/\b([A-Z][A-Z0-9_]*_(?:SECRET|TOKEN|KEY|PASSWORD|PASSWD))\b/);
|
|
1032
|
+
if (valMatch && nameMatch) {
|
|
1033
|
+
const value = valMatch[1];
|
|
1034
|
+
const isKeySuffix = nameMatch[1].endsWith("_KEY");
|
|
1035
|
+
const isKebabIdentifier = value.length < 40 && /^[a-z0-9]+(-[a-z0-9]+)+$/.test(value);
|
|
1036
|
+
if (isKeySuffix && isKebabIdentifier) continue;
|
|
1037
|
+
}
|
|
1038
|
+
}
|
|
1018
1039
|
matches.push(rm);
|
|
1019
1040
|
}
|
|
1020
1041
|
}
|
|
@@ -1204,11 +1225,19 @@ var sqlInjection = {
|
|
|
1204
1225
|
// The older `[^"']*` class bailed out at the first inner quote and
|
|
1205
1226
|
// missed every realistic SQL-containing concat.
|
|
1206
1227
|
/(?:query|execute|raw|sql|queryRaw|queryRawUnsafe|executeRaw|executeRawUnsafe)\s*\(\s*(?:"(?:[^"\\]|\\.)*"|'(?:[^'\\]|\\.)*')\s*\+/gi,
|
|
1207
|
-
// Direct variable interpolation in a SQL
|
|
1208
|
-
/
|
|
1228
|
+
// Direct variable interpolation in a SQL string literal. Requires the
|
|
1229
|
+
// verb to START a quoted/backtick literal AND be followed — within the
|
|
1230
|
+
// same literal — by a SQL structural keyword (FROM/SET/WHERE/…), then a
|
|
1231
|
+
// `${` interpolation. The structural keyword is what distinguishes a
|
|
1232
|
+
// real query from ordinary prose: the previous pattern matched any
|
|
1233
|
+
// SELECT/UPDATE/… word anywhere on a line followed by `${`, so UI copy
|
|
1234
|
+
// like `UPDATE your profile settings ${name}` flagged as a CRITICAL SQL
|
|
1235
|
+
// injection. Real queries ("SELECT … FROM …", "UPDATE … SET …") always
|
|
1236
|
+
// carry a second structural keyword; sentences almost never do.
|
|
1237
|
+
/["'`]\s*(?:SELECT|INSERT|UPDATE|DELETE)\b[^"'`\n]*\b(?:FROM|INTO|SET|VALUES|WHERE|JOIN|RETURNING)\b[^"'`\n]*\$\{(?!.*parameterized)/gi
|
|
1209
1238
|
];
|
|
1210
1239
|
const matches = [];
|
|
1211
|
-
const usesParams = /\?\s
|
|
1240
|
+
const usesParams = /\?\s*,|\?[^?,;]{1,20}\?|\$\d+|:[\w]+|\bprepare\b|\bplaceholders?\b/i.test(content);
|
|
1212
1241
|
if (usesParams) return [];
|
|
1213
1242
|
for (const pattern of patterns) {
|
|
1214
1243
|
const raw = findMatches(
|
|
@@ -1251,6 +1280,7 @@ var xssVulnerability = {
|
|
|
1251
1280
|
// {@html} in Svelte
|
|
1252
1281
|
/\{@html\s/g
|
|
1253
1282
|
];
|
|
1283
|
+
const allLines = content.split("\n");
|
|
1254
1284
|
const matches = [];
|
|
1255
1285
|
for (const pattern of patterns) {
|
|
1256
1286
|
const raw = findMatches(
|
|
@@ -1258,12 +1288,18 @@ var xssVulnerability = {
|
|
|
1258
1288
|
pattern,
|
|
1259
1289
|
xssVulnerability,
|
|
1260
1290
|
filePath,
|
|
1261
|
-
() => "Sanitize user input before rendering as HTML. Use a library like DOMPurify: DOMPurify.sanitize(userInput)"
|
|
1291
|
+
() => "Sanitize user input before rendering as HTML. Use a library like DOMPurify: DOMPurify.sanitize(userInput). If this site is intentional (JSON-LD structured data, a const boot script, a sandboxed preview), add an inline `// VC007-OK: <reason>` comment above the line to silence."
|
|
1262
1292
|
);
|
|
1263
1293
|
for (const m of raw) {
|
|
1264
|
-
const lineText =
|
|
1294
|
+
const lineText = allLines[m.line - 1] || "";
|
|
1265
1295
|
if (/\.innerHTML\s*=\s*['"]/.test(lineText) && !/\$\{/.test(lineText)) continue;
|
|
1266
1296
|
if (/\.innerHTML\s*=\s*['"][^'"]*['"]\s*$/.test(lineText)) continue;
|
|
1297
|
+
if (/dangerouslySetInnerHTML/.test(lineText)) {
|
|
1298
|
+
const windowText = allLines.slice(m.line - 1, m.line + 2).join("\n");
|
|
1299
|
+
if (/JSON\.stringify/i.test(windowText)) continue;
|
|
1300
|
+
const lineStartIdx = allLines.slice(0, m.line - 1).reduce((acc, l) => acc + l.length + 1, 0);
|
|
1301
|
+
if (isInlineSilenced(content, lineStartIdx, "VC007")) continue;
|
|
1302
|
+
}
|
|
1267
1303
|
matches.push(m);
|
|
1268
1304
|
}
|
|
1269
1305
|
}
|
|
@@ -1539,23 +1575,35 @@ var unvalidatedRedirect = {
|
|
|
1539
1575
|
check(content, filePath) {
|
|
1540
1576
|
if (isTestFile(filePath)) return [];
|
|
1541
1577
|
if (/isAllowedRedirect|validateRedirect|isSafeRedirect|allowedDomains|trustedDomains|whitelist.*url|allowlist.*url/i.test(content)) return [];
|
|
1542
|
-
const patterns = [
|
|
1543
|
-
/window\.location\s*=\s*(?!["'`]https?:\/\/)/g,
|
|
1544
|
-
/window\.location\.href\s*=\s*(?!["'`]https?:\/\/)/g,
|
|
1545
|
-
/window\.location\.assign\s*\(\s*(?!["'`]https?:\/\/)/g,
|
|
1546
|
-
/window\.location\.replace\s*\(\s*(?!["'`]https?:\/\/)/g,
|
|
1547
|
-
/res\.redirect\s*\(\s*(?:req\.|params\.|query\.)/gi
|
|
1548
|
-
];
|
|
1549
1578
|
const matches = [];
|
|
1550
|
-
|
|
1551
|
-
|
|
1552
|
-
|
|
1553
|
-
|
|
1554
|
-
|
|
1555
|
-
|
|
1556
|
-
|
|
1557
|
-
)
|
|
1579
|
+
const navPattern = /(?:window\.location(?:\.href)?\s*=\s*|window\.location\.(?:assign|replace)\s*\(\s*)([^;\n)]+)/g;
|
|
1580
|
+
let m;
|
|
1581
|
+
while ((m = navPattern.exec(content)) !== null) {
|
|
1582
|
+
if (isCommentLine(content, m.index)) continue;
|
|
1583
|
+
const value = m[1].trim();
|
|
1584
|
+
const hasInterpolation = /\$\{/.test(value);
|
|
1585
|
+
const isPureLiteral = /^["'`][^"'`]*["'`]$/.test(value);
|
|
1586
|
+
if (!hasInterpolation && isPureLiteral) continue;
|
|
1587
|
+
if (isInlineSilenced(content, m.index, "VC016")) continue;
|
|
1588
|
+
const line = content.substring(0, m.index).split("\n").length;
|
|
1589
|
+
matches.push({
|
|
1590
|
+
rule: "VC016",
|
|
1591
|
+
title: unvalidatedRedirect.title,
|
|
1592
|
+
severity: "high",
|
|
1593
|
+
category: "Injection",
|
|
1594
|
+
file: filePath,
|
|
1595
|
+
line,
|
|
1596
|
+
snippet: getSnippet(content, line),
|
|
1597
|
+
fix: "Validate redirect URLs against an allowlist of trusted domains. Never redirect to user-supplied URLs directly. If the target is a hardcoded/internal path you control, add an inline `// VC016-OK: <reason>` comment to silence."
|
|
1598
|
+
});
|
|
1558
1599
|
}
|
|
1600
|
+
matches.push(...findMatches(
|
|
1601
|
+
content,
|
|
1602
|
+
/res\.redirect\s*\(\s*(?:req\.|params\.|query\.)/gi,
|
|
1603
|
+
unvalidatedRedirect,
|
|
1604
|
+
filePath,
|
|
1605
|
+
() => "Validate redirect URLs against an allowlist of trusted domains. Never redirect to user-supplied URLs directly."
|
|
1606
|
+
));
|
|
1559
1607
|
return matches;
|
|
1560
1608
|
}
|
|
1561
1609
|
};
|
|
@@ -3299,6 +3347,7 @@ var dangerousInnerHTML = {
|
|
|
3299
3347
|
let m;
|
|
3300
3348
|
while ((m = re.exec(content)) !== null) {
|
|
3301
3349
|
if (isCommentLine(content, m.index)) continue;
|
|
3350
|
+
if (isInlineSilenced(content, m.index, "VC063")) continue;
|
|
3302
3351
|
const value = m[1].trim();
|
|
3303
3352
|
if (/^[A-Z][A-Z0-9_]+$/.test(value)) continue;
|
|
3304
3353
|
if (/^["'`][^$]*["'`]$/.test(value)) continue;
|
|
@@ -3312,7 +3361,7 @@ var dangerousInnerHTML = {
|
|
|
3312
3361
|
file: filePath,
|
|
3313
3362
|
line: lineNum,
|
|
3314
3363
|
snippet: getSnippet(content, lineNum),
|
|
3315
|
-
fix: "Sanitize HTML before using dangerouslySetInnerHTML: dangerouslySetInnerHTML={{ __html: DOMPurify.sanitize(content) }}. Install: npm install dompurify"
|
|
3364
|
+
fix: "Sanitize HTML before using dangerouslySetInnerHTML: dangerouslySetInnerHTML={{ __html: DOMPurify.sanitize(content) }}. Install: npm install dompurify. If this site is intentional (server-built template, nonce-attribute boot script, etc.), add an inline `// VC063-OK: <reason>` comment above the line to silence."
|
|
3316
3365
|
});
|
|
3317
3366
|
}
|
|
3318
3367
|
return findings;
|
|
@@ -7314,6 +7363,7 @@ var secretInURLParam = {
|
|
|
7314
7363
|
while ((m = re.exec(content)) !== null) {
|
|
7315
7364
|
if (isCommentLine(content, m.index)) continue;
|
|
7316
7365
|
if (isInsideFixMessage(content, m.index)) continue;
|
|
7366
|
+
if (isInlineSilenced(content, m.index, "VC146")) continue;
|
|
7317
7367
|
const lineNum = content.substring(0, m.index).split("\n").length;
|
|
7318
7368
|
findings.push({
|
|
7319
7369
|
rule: "VC146",
|
|
@@ -7323,7 +7373,7 @@ var secretInURLParam = {
|
|
|
7323
7373
|
file: filePath,
|
|
7324
7374
|
line: lineNum,
|
|
7325
7375
|
snippet: getSnippet(content, lineNum),
|
|
7326
|
-
fix: "Pass secrets in the Authorization header (Bearer token) or request body, never in URL parameters. URL parameters are logged in server access logs, browser history, and referrer headers."
|
|
7376
|
+
fix: "Pass secrets in the Authorization header (Bearer token) or request body, never in URL parameters. URL parameters are logged in server access logs, browser history, and referrer headers. If this finding is legitimate (magic-link / claim-token flow), add an inline `// VC146-OK: <reason>` comment above the line to silence it."
|
|
7327
7377
|
});
|
|
7328
7378
|
}
|
|
7329
7379
|
}
|
|
@@ -7514,10 +7564,14 @@ var webhookSignatureVerification = {
|
|
|
7514
7564
|
check(content, filePath) {
|
|
7515
7565
|
if (!filePath.match(/\.(js|ts|jsx|tsx)$/)) return [];
|
|
7516
7566
|
if (isTestFile(filePath)) return [];
|
|
7517
|
-
if (
|
|
7567
|
+
if (!/webhook/i.test(filePath)) return [];
|
|
7518
7568
|
if (!/export\s+(?:async\s+)?function\s+POST/i.test(content)) return [];
|
|
7519
7569
|
const services = [
|
|
7520
|
-
|
|
7570
|
+
// Tightened Clerk events: require the specific subevent suffix
|
|
7571
|
+
// rather than bare `session.`. The bare-prefix version matched
|
|
7572
|
+
// Stripe Checkout `session.url`, which has nothing to do with
|
|
7573
|
+
// Clerk webhooks.
|
|
7574
|
+
{ name: "Clerk", content: /clerk/i, events: /user\.(created|updated|deleted)|session\.(created|removed|ended|revoked)/i, verify: /svix|Webhook\(\)\.verify|webhook-id|wh_secret/i },
|
|
7521
7575
|
{ name: "GitHub", content: /github/i, events: /push|pull_request|issues|installation/i, verify: /createHmac|x-hub-signature|verify.*signature|GITHUB_WEBHOOK_SECRET/i },
|
|
7522
7576
|
{ name: "Resend", content: /resend/i, events: /email\.sent|email\.delivered|email\.bounced/i, verify: /svix|webhook.*verify|x-webhook-signature|RESEND_WEBHOOK_SECRET/i },
|
|
7523
7577
|
{ name: "SendGrid", content: /sendgrid/i, events: /inbound.*parse|event.*webhook/i, verify: /EventWebhook|x-twilio-email|verifySignature|SENDGRID_WEBHOOK_VERIFICATION/i },
|
|
@@ -8269,11 +8323,20 @@ var SAFE_PATTERNS = [
|
|
|
8269
8323
|
/^'sha\d+-/,
|
|
8270
8324
|
/^nonce-/i,
|
|
8271
8325
|
// Embedded data URIs
|
|
8272
|
-
/^data:[a-z]+\//i
|
|
8326
|
+
/^data:[a-z]+\//i,
|
|
8327
|
+
// Sentry DSN — a publishable, client-side ingest key (it ships to the
|
|
8328
|
+
// browser), not a secret. Shape: https://<hash>@o<orgid>.ingest.<region.>
|
|
8329
|
+
// sentry.io/<projectid>. The generic URL allowlist above deliberately
|
|
8330
|
+
// excludes anything with an `@`, so the DSN would otherwise fire.
|
|
8331
|
+
/^https?:\/\/[0-9a-f]+@o\d+\.ingest\.[a-z0-9.]*sentry\.io\//i,
|
|
8332
|
+
// HTTP security-header directive values (HSTS, etc.) — `max-age=63072000;
|
|
8333
|
+
// includeSubDomains; preload`. Config strings a security scanner actively
|
|
8334
|
+
// tells users to add; never secrets.
|
|
8335
|
+
/^max-age=\d+/i
|
|
8273
8336
|
];
|
|
8274
8337
|
var SKIP_FILES = /\.(css|scss|less|svg|md|txt|html?|xml|yml|yaml|toml|lock|map|woff2?|ttf|eot|ico|png|jpg|gif|webp)$/i;
|
|
8275
8338
|
var SKIP_FILENAMES = /(?:package-lock\.json|pnpm-lock\.yaml|yarn\.lock|composer\.lock|Gemfile\.lock|Cargo\.lock|poetry\.lock|Pipfile\.lock|shrinkwrap\.json)$/i;
|
|
8276
|
-
var SAFE_VAR_NAMES = /(?:description|message|text|label|title|content|template|html|svg|css|style|class(?:Name)?|query|mutation|schema|regex|pattern|format|placeholder|comment|url|path|route|endpoint|href|src|alt|name|type|version|encoding|charset|locale|translation|copy|prose|markdown|slug|handle)/i;
|
|
8339
|
+
var SAFE_VAR_NAMES = /(?:description|message|text|label|title|content|template|html|svg|css|style|class(?:Name)?|query|mutation|schema|regex|pattern|format|placeholder|comment|url|path|route|endpoint|href|src|alt|name|type|version|encoding|charset|chars|alphabet|locale|translation|copy|prose|markdown|slug|handle)/i;
|
|
8277
8340
|
var HASH_LIKE_VAR_NAMES = /(?:^|[^a-z])(?:hash|digest|checksum|fingerprint|etag|crc|md5|sha1|sha256|sha512|contenthash|buildid|revision|commit|sri|integrity|cacheKey|fileHash|assetId|versionId)(?:$|[^a-z])/i;
|
|
8278
8341
|
var HASH_PREFIX_RE = /^(?:sha\d+|md5|crc32|base64|bcrypt|argon2|pbkdf2|blake2b?)[:_]/i;
|
|
8279
8342
|
var SECRET_VAR_NAMES = /(?:^|[^a-z])(?:key|secret|token|password|passwd|pwd|api[_-]?key|auth|credential|private|signing|bearer|access[_-]?token|refresh[_-]?token|session[_-]?id)(?:$|[^a-z])/i;
|
|
@@ -8307,6 +8370,7 @@ function scanEntropy(files) {
|
|
|
8307
8370
|
let match;
|
|
8308
8371
|
while ((match = stringPattern.exec(line)) !== null) {
|
|
8309
8372
|
const value = match[2];
|
|
8373
|
+
if (value.includes("${")) continue;
|
|
8310
8374
|
if (SAFE_PATTERNS.some((p) => p.test(value))) continue;
|
|
8311
8375
|
if (HASH_PREFIX_RE.test(value)) continue;
|
|
8312
8376
|
const beforeAssign = line.substring(0, match.index);
|