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.js
CHANGED
|
@@ -964,8 +964,16 @@ var sqlInjection = {
|
|
|
964
964
|
// The older `[^"']*` class bailed out at the first inner quote and
|
|
965
965
|
// missed every realistic SQL-containing concat.
|
|
966
966
|
/(?:query|execute|raw|sql|queryRaw|queryRawUnsafe|executeRaw|executeRawUnsafe)\s*\(\s*(?:"(?:[^"\\]|\\.)*"|'(?:[^'\\]|\\.)*')\s*\+/gi,
|
|
967
|
-
// Direct variable interpolation in a SQL
|
|
968
|
-
/
|
|
967
|
+
// Direct variable interpolation in a SQL string literal. Requires the
|
|
968
|
+
// verb to START a quoted/backtick literal AND be followed — within the
|
|
969
|
+
// same literal — by a SQL structural keyword (FROM/SET/WHERE/…), then a
|
|
970
|
+
// `${` interpolation. The structural keyword is what distinguishes a
|
|
971
|
+
// real query from ordinary prose: the previous pattern matched any
|
|
972
|
+
// SELECT/UPDATE/… word anywhere on a line followed by `${`, so UI copy
|
|
973
|
+
// like `UPDATE your profile settings ${name}` flagged as a CRITICAL SQL
|
|
974
|
+
// injection. Real queries ("SELECT … FROM …", "UPDATE … SET …") always
|
|
975
|
+
// carry a second structural keyword; sentences almost never do.
|
|
976
|
+
/["'`]\s*(?:SELECT|INSERT|UPDATE|DELETE)\b[^"'`\n]*\b(?:FROM|INTO|SET|VALUES|WHERE|JOIN|RETURNING)\b[^"'`\n]*\$\{(?!.*parameterized)/gi
|
|
969
977
|
];
|
|
970
978
|
const matches = [];
|
|
971
979
|
const usesParams = /\?\s*,|\?[^?,;]{1,20}\?|\$\d+|:[\w]+|\bprepare\b|\bplaceholders?\b/i.test(content);
|
|
@@ -1011,6 +1019,7 @@ var xssVulnerability = {
|
|
|
1011
1019
|
// {@html} in Svelte
|
|
1012
1020
|
/\{@html\s/g
|
|
1013
1021
|
];
|
|
1022
|
+
const allLines = content.split("\n");
|
|
1014
1023
|
const matches = [];
|
|
1015
1024
|
for (const pattern of patterns) {
|
|
1016
1025
|
const raw = findMatches(
|
|
@@ -1018,12 +1027,18 @@ var xssVulnerability = {
|
|
|
1018
1027
|
pattern,
|
|
1019
1028
|
xssVulnerability,
|
|
1020
1029
|
filePath,
|
|
1021
|
-
() => "Sanitize user input before rendering as HTML. Use a library like DOMPurify: DOMPurify.sanitize(userInput)"
|
|
1030
|
+
() => "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."
|
|
1022
1031
|
);
|
|
1023
1032
|
for (const m of raw) {
|
|
1024
|
-
const lineText =
|
|
1033
|
+
const lineText = allLines[m.line - 1] || "";
|
|
1025
1034
|
if (/\.innerHTML\s*=\s*['"]/.test(lineText) && !/\$\{/.test(lineText)) continue;
|
|
1026
1035
|
if (/\.innerHTML\s*=\s*['"][^'"]*['"]\s*$/.test(lineText)) continue;
|
|
1036
|
+
if (/dangerouslySetInnerHTML/.test(lineText)) {
|
|
1037
|
+
const windowText = allLines.slice(m.line - 1, m.line + 2).join("\n");
|
|
1038
|
+
if (/JSON\.stringify/i.test(windowText)) continue;
|
|
1039
|
+
const lineStartIdx = allLines.slice(0, m.line - 1).reduce((acc, l) => acc + l.length + 1, 0);
|
|
1040
|
+
if (isInlineSilenced(content, lineStartIdx, "VC007")) continue;
|
|
1041
|
+
}
|
|
1027
1042
|
matches.push(m);
|
|
1028
1043
|
}
|
|
1029
1044
|
}
|
|
@@ -1299,23 +1314,35 @@ var unvalidatedRedirect = {
|
|
|
1299
1314
|
check(content, filePath) {
|
|
1300
1315
|
if (isTestFile(filePath)) return [];
|
|
1301
1316
|
if (/isAllowedRedirect|validateRedirect|isSafeRedirect|allowedDomains|trustedDomains|whitelist.*url|allowlist.*url/i.test(content)) return [];
|
|
1302
|
-
const patterns = [
|
|
1303
|
-
/window\.location\s*=\s*(?!["'`]https?:\/\/)/g,
|
|
1304
|
-
/window\.location\.href\s*=\s*(?!["'`]https?:\/\/)/g,
|
|
1305
|
-
/window\.location\.assign\s*\(\s*(?!["'`]https?:\/\/)/g,
|
|
1306
|
-
/window\.location\.replace\s*\(\s*(?!["'`]https?:\/\/)/g,
|
|
1307
|
-
/res\.redirect\s*\(\s*(?:req\.|params\.|query\.)/gi
|
|
1308
|
-
];
|
|
1309
1317
|
const matches = [];
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
)
|
|
1318
|
+
const navPattern = /(?:window\.location(?:\.href)?\s*=\s*|window\.location\.(?:assign|replace)\s*\(\s*)([^;\n)]+)/g;
|
|
1319
|
+
let m;
|
|
1320
|
+
while ((m = navPattern.exec(content)) !== null) {
|
|
1321
|
+
if (isCommentLine(content, m.index)) continue;
|
|
1322
|
+
const value = m[1].trim();
|
|
1323
|
+
const hasInterpolation = /\$\{/.test(value);
|
|
1324
|
+
const isPureLiteral = /^["'`][^"'`]*["'`]$/.test(value);
|
|
1325
|
+
if (!hasInterpolation && isPureLiteral) continue;
|
|
1326
|
+
if (isInlineSilenced(content, m.index, "VC016")) continue;
|
|
1327
|
+
const line = content.substring(0, m.index).split("\n").length;
|
|
1328
|
+
matches.push({
|
|
1329
|
+
rule: "VC016",
|
|
1330
|
+
title: unvalidatedRedirect.title,
|
|
1331
|
+
severity: "high",
|
|
1332
|
+
category: "Injection",
|
|
1333
|
+
file: filePath,
|
|
1334
|
+
line,
|
|
1335
|
+
snippet: getSnippet(content, line),
|
|
1336
|
+
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."
|
|
1337
|
+
});
|
|
1318
1338
|
}
|
|
1339
|
+
matches.push(...findMatches(
|
|
1340
|
+
content,
|
|
1341
|
+
/res\.redirect\s*\(\s*(?:req\.|params\.|query\.)/gi,
|
|
1342
|
+
unvalidatedRedirect,
|
|
1343
|
+
filePath,
|
|
1344
|
+
() => "Validate redirect URLs against an allowlist of trusted domains. Never redirect to user-supplied URLs directly."
|
|
1345
|
+
));
|
|
1319
1346
|
return matches;
|
|
1320
1347
|
}
|
|
1321
1348
|
};
|
|
@@ -1854,9 +1881,15 @@ var insecureDeserialization = {
|
|
|
1854
1881
|
() => "Never deserialize untrusted data. Use JSON instead of pickle/Marshal/unserialize. For YAML, use yaml.safe_load(). Validate and sanitize all input before deserialization."
|
|
1855
1882
|
));
|
|
1856
1883
|
}
|
|
1884
|
+
const isJsTs = /\.(jsx?|tsx?|mjs|cjs)$/.test(filePath);
|
|
1857
1885
|
return matches.filter((m) => {
|
|
1858
1886
|
if (!/yaml\.load\s*\(/.test(m.snippet ?? "")) return true;
|
|
1859
1887
|
const lineText = (m.snippet ?? "").toLowerCase();
|
|
1888
|
+
if (isJsTs) {
|
|
1889
|
+
const ctxLines = content.split("\n").slice(m.line - 1, m.line + 2).join("\n");
|
|
1890
|
+
if (/default_full_schema|\bfull_schema\b/i.test(ctxLines)) return true;
|
|
1891
|
+
return false;
|
|
1892
|
+
}
|
|
1860
1893
|
if (/safe_schema|failsafe_schema|safe_load|safeloader/.test(lineText)) {
|
|
1861
1894
|
return false;
|
|
1862
1895
|
}
|
|
@@ -3624,7 +3657,11 @@ var xxeVulnerability = {
|
|
|
3624
3657
|
const patterns = [
|
|
3625
3658
|
/\.parseXm?l\s*\(/gi,
|
|
3626
3659
|
// catches parseXml (libxmljs) AND parseXML
|
|
3627
|
-
|
|
3660
|
+
// NOTE: the browser `new DOMParser()` is intentionally NOT flagged.
|
|
3661
|
+
// Per the HTML/XML spec, DOMParser.parseFromString does not resolve
|
|
3662
|
+
// external entities, so it is not an XXE sink — flagging it produced a
|
|
3663
|
+
// critical false positive on ordinary client-side XML/HTML parsing.
|
|
3664
|
+
// Real XXE sinks (libxmljs parseXml with noent, etree, SAX) remain below.
|
|
3628
3665
|
/etree\.parse\s*\(/g,
|
|
3629
3666
|
/lxml\.etree/g,
|
|
3630
3667
|
/SAXParserFactory/g,
|
|
@@ -6795,7 +6832,11 @@ var llmOutputAsHTML = {
|
|
|
6795
6832
|
const findings = [];
|
|
6796
6833
|
const patterns = [
|
|
6797
6834
|
// dangerouslySetInnerHTML with .choices[0].message.content / .text / etc.
|
|
6798
|
-
|
|
6835
|
+
// NOTE: a bare `text` token used to be in this alternation and matched
|
|
6836
|
+
// any `.text` property (e.g. `post.text`) in a file that merely imported
|
|
6837
|
+
// an LLM SDK — a high-severity false positive. Only LLM-specific shapes
|
|
6838
|
+
// remain (delta.text / output_text / generated_text are qualified).
|
|
6839
|
+
/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,
|
|
6799
6840
|
// .innerHTML = response.choices[0].message.content
|
|
6800
6841
|
/\.innerHTML\s*=\s*[^;]*\b(?:choices\[\d*\]?\.message|completion|response\.message|message\.content|delta\.text|generated_text|output_text)\b/g
|
|
6801
6842
|
];
|
|
@@ -8035,11 +8076,20 @@ var SAFE_PATTERNS = [
|
|
|
8035
8076
|
/^'sha\d+-/,
|
|
8036
8077
|
/^nonce-/i,
|
|
8037
8078
|
// Embedded data URIs
|
|
8038
|
-
/^data:[a-z]+\//i
|
|
8079
|
+
/^data:[a-z]+\//i,
|
|
8080
|
+
// Sentry DSN — a publishable, client-side ingest key (it ships to the
|
|
8081
|
+
// browser), not a secret. Shape: https://<hash>@o<orgid>.ingest.<region.>
|
|
8082
|
+
// sentry.io/<projectid>. The generic URL allowlist above deliberately
|
|
8083
|
+
// excludes anything with an `@`, so the DSN would otherwise fire.
|
|
8084
|
+
/^https?:\/\/[0-9a-f]+@o\d+\.ingest\.[a-z0-9.]*sentry\.io\//i,
|
|
8085
|
+
// HTTP security-header directive values (HSTS, etc.) — `max-age=63072000;
|
|
8086
|
+
// includeSubDomains; preload`. Config strings a security scanner actively
|
|
8087
|
+
// tells users to add; never secrets.
|
|
8088
|
+
/^max-age=\d+/i
|
|
8039
8089
|
];
|
|
8040
8090
|
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;
|
|
8041
8091
|
var SKIP_FILENAMES = /(?:package-lock\.json|pnpm-lock\.yaml|yarn\.lock|composer\.lock|Gemfile\.lock|Cargo\.lock|poetry\.lock|Pipfile\.lock|shrinkwrap\.json)$/i;
|
|
8042
|
-
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;
|
|
8092
|
+
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;
|
|
8043
8093
|
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;
|
|
8044
8094
|
var HASH_PREFIX_RE = /^(?:sha\d+|md5|crc32|base64|bcrypt|argon2|pbkdf2|blake2b?)[:_]/i;
|
|
8045
8095
|
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;
|
|
@@ -8073,6 +8123,7 @@ function scanEntropy(files) {
|
|
|
8073
8123
|
let match;
|
|
8074
8124
|
while ((match = stringPattern.exec(line)) !== null) {
|
|
8075
8125
|
const value = match[2];
|
|
8126
|
+
if (value.includes("${")) continue;
|
|
8076
8127
|
if (SAFE_PATTERNS.some((p) => p.test(value))) continue;
|
|
8077
8128
|
if (HASH_PREFIX_RE.test(value)) continue;
|
|
8078
8129
|
const beforeAssign = line.substring(0, match.index);
|