xploitscan-shared-rules 1.7.0 → 1.7.3
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 +74 -23
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +74 -23
- 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
|
};
|
|
@@ -2115,9 +2142,15 @@ var insecureDeserialization = {
|
|
|
2115
2142
|
() => "Never deserialize untrusted data. Use JSON instead of pickle/Marshal/unserialize. For YAML, use yaml.safe_load(). Validate and sanitize all input before deserialization."
|
|
2116
2143
|
));
|
|
2117
2144
|
}
|
|
2145
|
+
const isJsTs = /\.(jsx?|tsx?|mjs|cjs)$/.test(filePath);
|
|
2118
2146
|
return matches.filter((m) => {
|
|
2119
2147
|
if (!/yaml\.load\s*\(/.test(m.snippet ?? "")) return true;
|
|
2120
2148
|
const lineText = (m.snippet ?? "").toLowerCase();
|
|
2149
|
+
if (isJsTs) {
|
|
2150
|
+
const ctxLines = content.split("\n").slice(m.line - 1, m.line + 2).join("\n");
|
|
2151
|
+
if (/default_full_schema|\bfull_schema\b/i.test(ctxLines)) return true;
|
|
2152
|
+
return false;
|
|
2153
|
+
}
|
|
2121
2154
|
if (/safe_schema|failsafe_schema|safe_load|safeloader/.test(lineText)) {
|
|
2122
2155
|
return false;
|
|
2123
2156
|
}
|
|
@@ -3885,7 +3918,11 @@ var xxeVulnerability = {
|
|
|
3885
3918
|
const patterns = [
|
|
3886
3919
|
/\.parseXm?l\s*\(/gi,
|
|
3887
3920
|
// catches parseXml (libxmljs) AND parseXML
|
|
3888
|
-
|
|
3921
|
+
// NOTE: the browser `new DOMParser()` is intentionally NOT flagged.
|
|
3922
|
+
// Per the HTML/XML spec, DOMParser.parseFromString does not resolve
|
|
3923
|
+
// external entities, so it is not an XXE sink — flagging it produced a
|
|
3924
|
+
// critical false positive on ordinary client-side XML/HTML parsing.
|
|
3925
|
+
// Real XXE sinks (libxmljs parseXml with noent, etree, SAX) remain below.
|
|
3889
3926
|
/etree\.parse\s*\(/g,
|
|
3890
3927
|
/lxml\.etree/g,
|
|
3891
3928
|
/SAXParserFactory/g,
|
|
@@ -7056,7 +7093,11 @@ var llmOutputAsHTML = {
|
|
|
7056
7093
|
const findings = [];
|
|
7057
7094
|
const patterns = [
|
|
7058
7095
|
// dangerouslySetInnerHTML with .choices[0].message.content / .text / etc.
|
|
7059
|
-
|
|
7096
|
+
// NOTE: a bare `text` token used to be in this alternation and matched
|
|
7097
|
+
// any `.text` property (e.g. `post.text`) in a file that merely imported
|
|
7098
|
+
// an LLM SDK — a high-severity false positive. Only LLM-specific shapes
|
|
7099
|
+
// remain (delta.text / output_text / generated_text are qualified).
|
|
7100
|
+
/dangerouslySetInnerHTML\s*=\s*\{\{\s*__html\s*:\s*[^}]*\b(?:choices\[\d*\]?\.message|completion|response|message\.content|content_block|delta\.text|generated_text|output_text)\b/g,
|
|
7060
7101
|
// .innerHTML = response.choices[0].message.content
|
|
7061
7102
|
/\.innerHTML\s*=\s*[^;]*\b(?:choices\[\d*\]?\.message|completion|response\.message|message\.content|delta\.text|generated_text|output_text)\b/g
|
|
7062
7103
|
];
|
|
@@ -8296,11 +8337,20 @@ var SAFE_PATTERNS = [
|
|
|
8296
8337
|
/^'sha\d+-/,
|
|
8297
8338
|
/^nonce-/i,
|
|
8298
8339
|
// Embedded data URIs
|
|
8299
|
-
/^data:[a-z]+\//i
|
|
8340
|
+
/^data:[a-z]+\//i,
|
|
8341
|
+
// Sentry DSN — a publishable, client-side ingest key (it ships to the
|
|
8342
|
+
// browser), not a secret. Shape: https://<hash>@o<orgid>.ingest.<region.>
|
|
8343
|
+
// sentry.io/<projectid>. The generic URL allowlist above deliberately
|
|
8344
|
+
// excludes anything with an `@`, so the DSN would otherwise fire.
|
|
8345
|
+
/^https?:\/\/[0-9a-f]+@o\d+\.ingest\.[a-z0-9.]*sentry\.io\//i,
|
|
8346
|
+
// HTTP security-header directive values (HSTS, etc.) — `max-age=63072000;
|
|
8347
|
+
// includeSubDomains; preload`. Config strings a security scanner actively
|
|
8348
|
+
// tells users to add; never secrets.
|
|
8349
|
+
/^max-age=\d+/i
|
|
8300
8350
|
];
|
|
8301
8351
|
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
8352
|
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;
|
|
8353
|
+
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
8354
|
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
8355
|
var HASH_PREFIX_RE = /^(?:sha\d+|md5|crc32|base64|bcrypt|argon2|pbkdf2|blake2b?)[:_]/i;
|
|
8306
8356
|
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 +8384,7 @@ function scanEntropy(files) {
|
|
|
8334
8384
|
let match;
|
|
8335
8385
|
while ((match = stringPattern.exec(line)) !== null) {
|
|
8336
8386
|
const value = match[2];
|
|
8387
|
+
if (value.includes("${")) continue;
|
|
8337
8388
|
if (SAFE_PATTERNS.some((p) => p.test(value))) continue;
|
|
8338
8389
|
if (HASH_PREFIX_RE.test(value)) continue;
|
|
8339
8390
|
const beforeAssign = line.substring(0, match.index);
|