xploitscan-shared-rules 1.7.0 → 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 +58 -21
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +58 -21
- package/dist/index.js.map +1 -1
- package/package.json +5 -5
package/dist/index.cjs
CHANGED
|
@@ -1225,8 +1225,16 @@ var sqlInjection = {
|
|
|
1225
1225
|
// The older `[^"']*` class bailed out at the first inner quote and
|
|
1226
1226
|
// missed every realistic SQL-containing concat.
|
|
1227
1227
|
/(?:query|execute|raw|sql|queryRaw|queryRawUnsafe|executeRaw|executeRawUnsafe)\s*\(\s*(?:"(?:[^"\\]|\\.)*"|'(?:[^'\\]|\\.)*')\s*\+/gi,
|
|
1228
|
-
// Direct variable interpolation in a SQL
|
|
1229
|
-
/
|
|
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
|
|
1230
1238
|
];
|
|
1231
1239
|
const matches = [];
|
|
1232
1240
|
const usesParams = /\?\s*,|\?[^?,;]{1,20}\?|\$\d+|:[\w]+|\bprepare\b|\bplaceholders?\b/i.test(content);
|
|
@@ -1272,6 +1280,7 @@ var xssVulnerability = {
|
|
|
1272
1280
|
// {@html} in Svelte
|
|
1273
1281
|
/\{@html\s/g
|
|
1274
1282
|
];
|
|
1283
|
+
const allLines = content.split("\n");
|
|
1275
1284
|
const matches = [];
|
|
1276
1285
|
for (const pattern of patterns) {
|
|
1277
1286
|
const raw = findMatches(
|
|
@@ -1279,12 +1288,18 @@ var xssVulnerability = {
|
|
|
1279
1288
|
pattern,
|
|
1280
1289
|
xssVulnerability,
|
|
1281
1290
|
filePath,
|
|
1282
|
-
() => "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."
|
|
1283
1292
|
);
|
|
1284
1293
|
for (const m of raw) {
|
|
1285
|
-
const lineText =
|
|
1294
|
+
const lineText = allLines[m.line - 1] || "";
|
|
1286
1295
|
if (/\.innerHTML\s*=\s*['"]/.test(lineText) && !/\$\{/.test(lineText)) continue;
|
|
1287
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
|
+
}
|
|
1288
1303
|
matches.push(m);
|
|
1289
1304
|
}
|
|
1290
1305
|
}
|
|
@@ -1560,23 +1575,35 @@ var unvalidatedRedirect = {
|
|
|
1560
1575
|
check(content, filePath) {
|
|
1561
1576
|
if (isTestFile(filePath)) return [];
|
|
1562
1577
|
if (/isAllowedRedirect|validateRedirect|isSafeRedirect|allowedDomains|trustedDomains|whitelist.*url|allowlist.*url/i.test(content)) return [];
|
|
1563
|
-
const patterns = [
|
|
1564
|
-
/window\.location\s*=\s*(?!["'`]https?:\/\/)/g,
|
|
1565
|
-
/window\.location\.href\s*=\s*(?!["'`]https?:\/\/)/g,
|
|
1566
|
-
/window\.location\.assign\s*\(\s*(?!["'`]https?:\/\/)/g,
|
|
1567
|
-
/window\.location\.replace\s*\(\s*(?!["'`]https?:\/\/)/g,
|
|
1568
|
-
/res\.redirect\s*\(\s*(?:req\.|params\.|query\.)/gi
|
|
1569
|
-
];
|
|
1570
1578
|
const matches = [];
|
|
1571
|
-
|
|
1572
|
-
|
|
1573
|
-
|
|
1574
|
-
|
|
1575
|
-
|
|
1576
|
-
|
|
1577
|
-
|
|
1578
|
-
)
|
|
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
|
+
});
|
|
1579
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
|
+
));
|
|
1580
1607
|
return matches;
|
|
1581
1608
|
}
|
|
1582
1609
|
};
|
|
@@ -8296,11 +8323,20 @@ var SAFE_PATTERNS = [
|
|
|
8296
8323
|
/^'sha\d+-/,
|
|
8297
8324
|
/^nonce-/i,
|
|
8298
8325
|
// Embedded data URIs
|
|
8299
|
-
/^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
|
|
8300
8336
|
];
|
|
8301
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;
|
|
8302
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;
|
|
8303
|
-
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;
|
|
8304
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;
|
|
8305
8341
|
var HASH_PREFIX_RE = /^(?:sha\d+|md5|crc32|base64|bcrypt|argon2|pbkdf2|blake2b?)[:_]/i;
|
|
8306
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;
|
|
@@ -8334,6 +8370,7 @@ function scanEntropy(files) {
|
|
|
8334
8370
|
let match;
|
|
8335
8371
|
while ((match = stringPattern.exec(line)) !== null) {
|
|
8336
8372
|
const value = match[2];
|
|
8373
|
+
if (value.includes("${")) continue;
|
|
8337
8374
|
if (SAFE_PATTERNS.some((p) => p.test(value))) continue;
|
|
8338
8375
|
if (HASH_PREFIX_RE.test(value)) continue;
|
|
8339
8376
|
const beforeAssign = line.substring(0, match.index);
|