@clear-capabilities/agentic-security-scanner 0.151.3 → 0.152.0
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/CHANGELOG.md +71 -0
- package/dist/agentic-security.mjs +80 -80
- package/dist/agentic-security.mjs.sha256 +1 -1
- package/package.json +5 -5
- package/src/dataflow/CLAUDE.md +1 -1
- package/src/dataflow/catalog.js +802 -13
- package/src/dataflow/engine.js +852 -89
- package/src/dataflow/incremental.js +13 -0
- package/src/dataflow/index.js +18 -5
- package/src/dataflow/path-feasibility.js +86 -9
- package/src/dataflow/string-domain.js +132 -0
- package/src/dataflow/summaries.js +69 -8
- package/src/engine.js +108 -3
- package/src/ir/CLAUDE.md +2 -1
- package/src/ir/balanced-call.js +33 -1
- package/src/ir/callgraph.js +26 -2
- package/src/ir/class-hierarchy.js +101 -6
- package/src/ir/csharp-ir.js +97 -8
- package/src/ir/index.js +21 -1
- package/src/ir/parser-cs.js +706 -24
- package/src/ir/parser-java.js +498 -17
- package/src/ir/parser-js.js +22 -0
- package/src/ir/parser-php.js +446 -43
- package/src/ir/parser-rust.js +1338 -0
- package/src/ir/php-include-merge.js +190 -0
- package/src/lineage/sink-registry.js +18 -0
- package/src/lineage/source-registry.js +23 -0
- package/src/posture/csharp-analysis.js +341 -43
- package/src/posture/finding-defaults.js +5 -1
- package/src/posture/fix-coverage.js +1 -0
- package/src/posture/proof-coverage.js +1 -0
- package/src/sast/CLAUDE.md +1 -1
- package/src/sast/authz.js +144 -1
- package/src/sast/crypto-protocol.js +65 -7
- package/src/sast/csharp-cross-file.js +125 -0
- package/src/sast/csharp-structural.js +78 -2
- package/src/sast/csharp.js +574 -42
- package/src/sast/java-ast-folding.js +268 -11
- package/src/sast/java-bench-extras.js +530 -2
- package/src/sast/java-deserialization.js +31 -2
- package/src/sast/java-structural-cross-file.js +146 -0
- package/src/sast/java-structural.js +499 -6
- package/src/sast/ldap-injection.js +133 -3
- package/src/sast/rust.js +57 -0
- package/src/sast/xss-reflected-multilang.js +252 -3
package/src/dataflow/catalog.js
CHANGED
|
@@ -243,6 +243,13 @@ export const CATALOG = [
|
|
|
243
243
|
// do not assume it is actually gating anything yet.
|
|
244
244
|
{ kind: 'source', id: 'java-resultset-getstring', language: 'java', framework: 'jdbc', match: { type: 'call', callee: 'getString', receiverTypeIn: ['resultset'] }, label: 'ResultSet.getString' },
|
|
245
245
|
{ kind: 'source', id: 'java-resultset-getobject', language: 'java', framework: 'jdbc', match: { type: 'call', callee: 'getObject', receiverTypeIn: ['resultset'] }, label: 'ResultSet.getObject' },
|
|
246
|
+
// SARD_80_F1_SCANNER_PRD.md Juliet Java audit: getAttribute/getQueryString
|
|
247
|
+
// are as common as getParameter in the servlet source set the catalog
|
|
248
|
+
// already covers, and Cookie.getValue is the standard way the array
|
|
249
|
+
// returned by the already-cataloged getCookies() is actually read.
|
|
250
|
+
{ kind: 'source', id: 'java-request-getAttribute', language: 'java', framework: 'servlet', match: { type: 'call', callee: 'getAttribute' }, label: 'request.getAttribute' },
|
|
251
|
+
{ kind: 'source', id: 'java-request-getQueryString', language: 'java', framework: 'servlet', match: { type: 'call', callee: 'getQueryString' }, label: 'request.getQueryString' },
|
|
252
|
+
{ kind: 'source', id: 'java-cookie-getvalue', language: 'java', framework: 'servlet', match: { type: 'call', callee: 'getValue' }, label: 'Cookie.getValue' },
|
|
246
253
|
|
|
247
254
|
// ─── SOURCES (Annotation/Decorator-shaped) ────────────────────────────────
|
|
248
255
|
// R14(a): annotation/decorator-shaped framework sources (Spring @RequestParam,
|
|
@@ -404,6 +411,28 @@ export const CATALOG = [
|
|
|
404
411
|
{ kind: 'source', id: 'php-symfony-content', language: 'php', framework: 'symfony', match: { type: 'call', callee: 'getContent' }, label: '$request->getContent() (Symfony)' },
|
|
405
412
|
{ kind: 'source', id: 'php-symfony-get', language: 'php', framework: 'symfony', match: { type: 'call', callee: 'get' }, label: '$request->get() (Symfony)' },
|
|
406
413
|
|
|
414
|
+
// SARD PHP corpus: file/stream/process-output sources. This corpus's
|
|
415
|
+
// dominant "source" shape is NOT a superglobal at all — it's a value read
|
|
416
|
+
// from a file or a subprocess's output (fgets on an fopen'd handle is the
|
|
417
|
+
// single most common first-source shape in the dev split, ahead of every
|
|
418
|
+
// superglobal combined), matching the real NIST SARD PHP Vulnerability
|
|
419
|
+
// Test Suite's own generator convention. Receiver-agnostic (`fgets($fh)`
|
|
420
|
+
// covers stdin, a socket, or an ordinary file — the taint concern is the
|
|
421
|
+
// same regardless of which handle) and file-content sources are real,
|
|
422
|
+
// legitimate untrusted input whenever the file/stream itself isn't a
|
|
423
|
+
// hardcoded, checked-in constant path.
|
|
424
|
+
{ kind: 'source', id: 'php-fgetc', language: 'php', framework: 'core', match: { type: 'call', callee: 'fgetc' }, label: 'fgetc()', provenance: 'file-read' },
|
|
425
|
+
{ kind: 'source', id: 'php-fscanf', language: 'php', framework: 'core', match: { type: 'call', callee: 'fscanf' }, label: 'fscanf()', provenance: 'file-read' },
|
|
426
|
+
{ kind: 'source', id: 'php-file-get-contents', language: 'php', framework: 'core', match: { type: 'call', callee: 'file_get_contents' }, label: 'file_get_contents()', provenance: 'file-read' },
|
|
427
|
+
{ kind: 'source', id: 'php-file', language: 'php', framework: 'core', match: { type: 'call', callee: 'file' }, label: 'file()', provenance: 'file-read' },
|
|
428
|
+
{ kind: 'source', id: 'php-stream-get-contents', language: 'php', framework: 'core', match: { type: 'call', callee: 'stream_get_contents' }, label: 'stream_get_contents()', provenance: 'file-read' },
|
|
429
|
+
{ kind: 'source', id: 'php-readline', language: 'php', framework: 'core', match: { type: 'call', callee: 'readline' }, label: 'readline()', provenance: 'stdin' },
|
|
430
|
+
// Assigned FROM (the return value of running a command), distinct from the
|
|
431
|
+
// sink use of the SAME functions (building a command from tainted input,
|
|
432
|
+
// see php-shell-exec/php-exec/php-system below) — both directions are real.
|
|
433
|
+
{ kind: 'source', id: 'php-shell-exec-output', language: 'php', framework: 'core', match: { type: 'call', callee: 'shell_exec' }, label: 'shell_exec() return value', provenance: 'file-read' },
|
|
434
|
+
{ kind: 'source', id: 'php-getenv', language: 'php', framework: 'core', match: { type: 'call', callee: 'getenv' }, label: 'getenv()', provenance: 'env' },
|
|
435
|
+
|
|
407
436
|
// ─── SINKS (SQL — Python) ─────────────────────────────────────────────────
|
|
408
437
|
{ kind: 'sink', id: 'py-cursor-execute', language: 'py', framework: 'dbapi', match: { type: 'call', callee: 'execute' }, argIndex: 0,
|
|
409
438
|
vuln: { name: 'SQL Injection (cursor.execute)', severity: 'critical', cwe: 'CWE-89',
|
|
@@ -432,6 +461,17 @@ export const CATALOG = [
|
|
|
432
461
|
remediation: 'Use PreparedStatement + setX(N, value).' } },
|
|
433
462
|
{ kind: 'sink', id: 'java-stmt-execute', language: 'java', framework: 'jdbc', match: { type: 'call', callee: 'execute' }, argIndex: 0,
|
|
434
463
|
vuln: { name: 'SQL Injection (Statement.execute)', severity: 'critical', cwe: 'CWE-89', remediation: 'Use PreparedStatement.' } },
|
|
464
|
+
// `Statement.addBatch(sql)` — the SQL string, not `executeBatch()` (which
|
|
465
|
+
// takes no arguments and just fires whatever was previously queued), is
|
|
466
|
+
// the actual sink: `stmt.addBatch("UPDATE ... WHERE name='" + tainted +
|
|
467
|
+
// "'"); ... stmt.executeBatch();`. Confirmed via the public Juliet Java
|
|
468
|
+
// mirror (CWE89_SQL_Injection's own executeBatch-suffixed variants, the
|
|
469
|
+
// single largest false-negative cluster this session found for CWE-89:
|
|
470
|
+
// 167 of 308 dev-split fns were this one missing sink). `argIndex: 0`
|
|
471
|
+
// naturally excludes `PreparedStatement.addBatch()`'s zero-arg safe form
|
|
472
|
+
// (queuing already-bound parameters) — there is no arg 0 to match.
|
|
473
|
+
{ kind: 'sink', id: 'java-stmt-addbatch', language: 'java', framework: 'jdbc', match: { type: 'call', callee: 'addBatch' }, argIndex: 0,
|
|
474
|
+
vuln: { name: 'SQL Injection (Statement.addBatch)', severity: 'critical', cwe: 'CWE-89', remediation: 'Use PreparedStatement + setX(N, value) + the zero-arg addBatch() form. Never concatenate user input into the SQL string passed to addBatch().' } },
|
|
435
475
|
{ kind: 'sink', id: 'java-jdbc-prepareStatement', language: 'java', framework: 'jdbc', match: { type: 'call', callee: 'prepareStatement' }, argIndex: 0,
|
|
436
476
|
vuln: { name: 'SQL Injection (PreparedStatement built via concat)', severity: 'critical', cwe: 'CWE-89',
|
|
437
477
|
remediation: 'Use placeholders (?) in the SQL string; bind values via setX(N, value).' } },
|
|
@@ -453,12 +493,51 @@ export const CATALOG = [
|
|
|
453
493
|
// response-write idiom, `HttpServletResponse.getWriter()`) is the
|
|
454
494
|
// dominant shape. Receiver-scoped to the `getWriter` chain segment (both
|
|
455
495
|
// `write` and `print` collide with countless unrelated APIs bare).
|
|
456
|
-
|
|
496
|
+
//
|
|
497
|
+
// SARD_80_F1 W4: the chained one-liner (`response.getWriter().write(x)`)
|
|
498
|
+
// was the ONLY shape these three entries could ever match — Java's more
|
|
499
|
+
// common idiom splits it in two (`PrintWriter out = response.getWriter();
|
|
500
|
+
// out.println(x);`), a completely different receiver (the local variable
|
|
501
|
+
// NAME, not the literal `getWriter` chain segment) that was invisible.
|
|
502
|
+
// Fixed via `receiverTypeIn` (ADDITIVE — confirms a match a failing name
|
|
503
|
+
// check missed, never replaces the name check), backed by
|
|
504
|
+
// `class-hierarchy.js`'s `typeOfVar` now also being seeded from a local's
|
|
505
|
+
// DECLARED type, not just `paramTypes`/`isNew` constructor calls (see its
|
|
506
|
+
// own comment). Deliberately NOT done via `parser-java.js`'s
|
|
507
|
+
// `_localVarConstructedTypes`/`_rewriteVarTypeCallees` string-rewrite
|
|
508
|
+
// mechanism — a first attempt did that and broke an EXISTING, unrelated
|
|
509
|
+
// catalog entry keyed on a short variable-name convention (`XPath xp = …;
|
|
510
|
+
// xp.evaluate(...)` — the rewrite mutated `xp.evaluate` to
|
|
511
|
+
// `XPath.evaluate`, and the xpath sink's `receiver: '^(?:xp|xpath)$'`
|
|
512
|
+
// stopped matching); confirmed by direct regression in
|
|
513
|
+
// `test/java-taint-flow.test.js`, then reverted. `println` was also
|
|
514
|
+
// completely absent from this list despite being the single most common
|
|
515
|
+
// of the three methods in real servlet code.
|
|
516
|
+
{ kind: 'sink', id: 'java-writer-write', language: 'java', framework: 'servlet', match: { type: 'call', callee: 'write', receiver: '^getWriter$', receiverTypeIn: ['^PrintWriter$'] }, argIndex: 0,
|
|
457
517
|
vuln: { name: 'Reflected XSS (PrintWriter.write)', severity: 'high', cwe: 'CWE-79',
|
|
458
518
|
remediation: 'HTML-escape user-derived content before writing to the response, or use a templating engine with auto-escaping.' } },
|
|
459
|
-
{ kind: 'sink', id: 'java-writer-print', language: 'java', framework: 'servlet', match: { type: 'call', callee: 'print', receiver: '^getWriter$' }, argIndex: 0,
|
|
519
|
+
{ kind: 'sink', id: 'java-writer-print', language: 'java', framework: 'servlet', match: { type: 'call', callee: 'print', receiver: '^getWriter$', receiverTypeIn: ['^PrintWriter$'] }, argIndex: 0,
|
|
460
520
|
vuln: { name: 'Reflected XSS (PrintWriter.print)', severity: 'high', cwe: 'CWE-79',
|
|
461
521
|
remediation: 'HTML-escape user-derived content before writing to the response, or use a templating engine with auto-escaping.' } },
|
|
522
|
+
{ kind: 'sink', id: 'java-writer-println', language: 'java', framework: 'servlet', match: { type: 'call', callee: 'println', receiver: '^getWriter$', receiverTypeIn: ['^PrintWriter$'] }, argIndex: 0,
|
|
523
|
+
vuln: { name: 'Reflected XSS (PrintWriter.println)', severity: 'high', cwe: 'CWE-79',
|
|
524
|
+
remediation: 'HTML-escape user-derived content before writing to the response, or use a templating engine with auto-escaping.' } },
|
|
525
|
+
// `HttpServletResponse.sendError(int, String)` — the message argument
|
|
526
|
+
// (argIndex 1, NOT 0) is written verbatim into the generated error page.
|
|
527
|
+
// Confirmed via the public Juliet Java mirror (CWE81_XSS_Error_Message's
|
|
528
|
+
// OWN corpus, 545 files, every one of them: `response.sendError(404,
|
|
529
|
+
// "<br>bad() - Parameter name has value " + data);`) that this — not a
|
|
530
|
+
// caught exception's `getMessage()`, which the CWE-81 name suggests and
|
|
531
|
+
// which a prior session-iteration built and verified working in
|
|
532
|
+
// isolation — is the sink Juliet's CWE-81 ("XSS in Error Message")
|
|
533
|
+
// actually exercises: "Error Message" names the `sendError` HTTP error
|
|
534
|
+
// message parameter, not an exception message. That earlier fix is not
|
|
535
|
+
// wrong (it is a real, separate gap for a shape this corpus just never
|
|
536
|
+
// happens to test) but it explains why CWE-81 showed zero real-corpus
|
|
537
|
+
// movement despite passing its own unit tests.
|
|
538
|
+
{ kind: 'sink', id: 'java-response-senderror', language: 'java', framework: 'servlet', match: { type: 'call', callee: 'sendError', receiver: '^response$', receiverTypeIn: ['^HttpServletResponse$'] }, argIndex: 1,
|
|
539
|
+
vuln: { name: 'Reflected XSS (HttpServletResponse.sendError message)', severity: 'high', cwe: 'CWE-79',
|
|
540
|
+
remediation: 'HTML-escape user-derived content before passing it as the sendError message, or send a generic, static error message with no user input.' } },
|
|
462
541
|
|
|
463
542
|
// ─── SINKS (SQL — Go) ──────────────────────────────────────────────────────
|
|
464
543
|
{ kind: 'sink', id: 'go-db-query', language: 'go', framework: 'database/sql', match: { type: 'call', callee: 'Query' }, argIndex: 0,
|
|
@@ -582,6 +661,17 @@ export const CATALOG = [
|
|
|
582
661
|
{ kind: 'sink', id: 'php-echo-xss', language: 'php', framework: 'core', match: { type: 'call', callee: '__php_echo__' }, argIndex: 'all',
|
|
583
662
|
vuln: { name: 'Reflected XSS (echo)', severity: 'high', cwe: 'CWE-79',
|
|
584
663
|
remediation: 'Escape output with htmlspecialchars() before echoing user-derived content.' } },
|
|
664
|
+
// SARD PHP corpus: the backtick shell-execution operator is a language
|
|
665
|
+
// construct like echo/print (parser-php.js now lowers it to a synthetic
|
|
666
|
+
// `__php_backtick_exec__` call, see that file for why).
|
|
667
|
+
{ kind: 'sink', id: 'php-backtick-exec', language: 'php', framework: 'core', match: { type: 'call', callee: '__php_backtick_exec__' }, argIndex: 'all',
|
|
668
|
+
vuln: { name: 'OS Command Injection (backtick operator)', severity: 'critical', cwe: 'CWE-78',
|
|
669
|
+
remediation: 'Never build a shell command from user input. Use escapeshellarg() on every interpolated value, or avoid shell_exec()/backticks entirely in favor of a fixed argv array passed to proc_open() or symfony/process.' } },
|
|
670
|
+
// include/require/include_once/require_once are language constructs
|
|
671
|
+
// (parser-php.js now lowers them to a synthetic `__php_include__` call).
|
|
672
|
+
{ kind: 'sink', id: 'php-include-lfi', language: 'php', framework: 'core', match: { type: 'call', callee: '__php_include__' }, argIndex: 'all',
|
|
673
|
+
vuln: { name: 'Local/Remote File Inclusion (include/require)', severity: 'critical', cwe: 'CWE-98',
|
|
674
|
+
remediation: 'Never include() / require() a user-controlled path. Build an explicit whitelist dispatch table instead: `$pages = ["home" => "home.php", ...]; include $pages[$_GET["page"]] ?? "404.php";`.' } },
|
|
585
675
|
|
|
586
676
|
// ─── SINKS (XSS / template — JS/TS / browser) ─────────────────────────────
|
|
587
677
|
{ kind: 'sink', id: 'js-innerHTML-assign', language: 'js', framework: 'dom', match: { type: 'member', object: '_any_', prop: 'innerHTML' }, argIndex: 'rhs',
|
|
@@ -792,6 +882,26 @@ export const CATALOG = [
|
|
|
792
882
|
{ kind: 'sink', id: 'java-new-File', language: 'java', framework: 'stdlib', match: { type: 'call', callee: 'File' }, argIndex: 0,
|
|
793
883
|
vuln: { name: 'Path Traversal (new File)', severity: 'high', cwe: 'CWE-22',
|
|
794
884
|
remediation: 'Canonicalize with Path.normalize + startsWith(base).' } },
|
|
885
|
+
// Juliet's own dominant file-sink shapes: `new FileInputStream(path)`,
|
|
886
|
+
// `new FileReader(path)`, `new RandomAccessFile(path, mode)`. Each is a
|
|
887
|
+
// distinct constructor (not an alias for `new File(...)`, which some
|
|
888
|
+
// callers wrap it around instead), so `java-new-File`'s bare `callee:
|
|
889
|
+
// 'File'` never matches any of them.
|
|
890
|
+
{ kind: 'sink', id: 'java-new-FileInputStream', language: 'java', framework: 'stdlib', match: { type: 'call', callee: 'FileInputStream' }, argIndex: 0,
|
|
891
|
+
vuln: { name: 'Path Traversal (new FileInputStream)', severity: 'high', cwe: 'CWE-22',
|
|
892
|
+
remediation: 'Canonicalize the path and verify it stays within an allow-listed base directory before opening it.' } },
|
|
893
|
+
{ kind: 'sink', id: 'java-new-FileOutputStream', language: 'java', framework: 'stdlib', match: { type: 'call', callee: 'FileOutputStream' }, argIndex: 0,
|
|
894
|
+
vuln: { name: 'Path Traversal (new FileOutputStream)', severity: 'high', cwe: 'CWE-22',
|
|
895
|
+
remediation: 'Canonicalize the path and verify it stays within an allow-listed base directory before opening it.' } },
|
|
896
|
+
{ kind: 'sink', id: 'java-new-FileReader', language: 'java', framework: 'stdlib', match: { type: 'call', callee: 'FileReader' }, argIndex: 0,
|
|
897
|
+
vuln: { name: 'Path Traversal (new FileReader)', severity: 'high', cwe: 'CWE-22',
|
|
898
|
+
remediation: 'Canonicalize the path and verify it stays within an allow-listed base directory before opening it.' } },
|
|
899
|
+
{ kind: 'sink', id: 'java-new-FileWriter', language: 'java', framework: 'stdlib', match: { type: 'call', callee: 'FileWriter' }, argIndex: 0,
|
|
900
|
+
vuln: { name: 'Path Traversal (new FileWriter)', severity: 'high', cwe: 'CWE-22',
|
|
901
|
+
remediation: 'Canonicalize the path and verify it stays within an allow-listed base directory before opening it.' } },
|
|
902
|
+
{ kind: 'sink', id: 'java-new-RandomAccessFile', language: 'java', framework: 'stdlib', match: { type: 'call', callee: 'RandomAccessFile' }, argIndex: 0,
|
|
903
|
+
vuln: { name: 'Path Traversal (new RandomAccessFile)', severity: 'high', cwe: 'CWE-22',
|
|
904
|
+
remediation: 'Canonicalize the path and verify it stays within an allow-listed base directory before opening it.' } },
|
|
795
905
|
{ kind: 'sink', id: 'go-os-open', language: 'go', framework: 'os', match: { type: 'call', callee: 'Open' }, argIndex: 0,
|
|
796
906
|
vuln: { name: 'Path Traversal (os.Open)', severity: 'high', cwe: 'CWE-22',
|
|
797
907
|
remediation: 'Use filepath.Clean + verify the path is rooted in your allow-list dir.' } },
|
|
@@ -848,7 +958,29 @@ export const CATALOG = [
|
|
|
848
958
|
// Taint-recall PRD (80%) Tier 1: XPath.evaluate() called directly (no
|
|
849
959
|
// .compile() call site to catch it via the entry above) — javax.xml.xpath's
|
|
850
960
|
// other common idiom. Receiver-scoped: "evaluate" is generic elsewhere.
|
|
851
|
-
|
|
961
|
+
//
|
|
962
|
+
// SARD_80_F1 W4.J32 — `xPath` (camelCase, matching the `XPath` type name
|
|
963
|
+
// exactly as `javax.xml.xpath.XPath`'s own Javadoc examples and the real
|
|
964
|
+
// public Juliet mirror's own CWE-643 files spell it — confirmed directly
|
|
965
|
+
// against `CWE643_Xpath_Injection__database_54e.java`'s
|
|
966
|
+
// `XPath xPath = XPathFactory.newInstance().newXPath();`) NEVER matched
|
|
967
|
+
// this entry: `_receiverAllowed` builds its regex with `new RegExp(pat)`,
|
|
968
|
+
// no `i` flag, so the case-sensitive `^(?:xp|xpath|expr)$` only ever
|
|
969
|
+
// matched the all-lowercase `xpath` or the short `xp`/`expr` — not the
|
|
970
|
+
// idiomatic camelCase form. This meant `IR-TAINT` could never fire on
|
|
971
|
+
// this sink for ANY Java file using ordinary naming conventions, in a
|
|
972
|
+
// single file or across any number of files — CWE-643's real corpus
|
|
973
|
+
// recall gap traced to cross-file/multi-hop chains (5 separate classes,
|
|
974
|
+
// confirmed via public-mirror `_54a`..`_54e`) was actually just the
|
|
975
|
+
// visible SYMPTOM: the structural/regex detectors that fired instead are
|
|
976
|
+
// inherently single-file, so once IR-TAINT (the only interprocedural
|
|
977
|
+
// mechanism) couldn't match this sink at all, every cross-file variant
|
|
978
|
+
// was invisible by construction. Verified directly: a same-file `xPath`
|
|
979
|
+
// repro fired ZERO `IR-TAINT` findings before this fix (structural/regex
|
|
980
|
+
// detectors covered it instead) and fired correctly after. `xp`/`xpath`/
|
|
981
|
+
// `expr` remain matched unchanged (existing `test/java-taint-flow.test.js`
|
|
982
|
+
// coverage for the `xp` short form is unaffected — purely additive).
|
|
983
|
+
{ kind: 'sink', id: 'java-xpath-evaluate', language: 'java', framework: 'xpath', match: { type: 'call', callee: 'evaluate', receiver: '^(?:xp|xpath|xPath|expr)$' }, argIndex: 0,
|
|
852
984
|
vuln: { name: 'XPath Injection (XPath.evaluate)', severity: 'high', cwe: 'CWE-643',
|
|
853
985
|
remediation: 'Use XPathVariableResolver or setXPathVariableResolver; never concat user input into the expression.' } },
|
|
854
986
|
{ kind: 'sink', id: 'cs-xml-selectnodes', language: 'cs', framework: 'stdlib', match: { type: 'call', callee: 'SelectNodes' }, argIndex: 0,
|
|
@@ -884,6 +1016,22 @@ export const CATALOG = [
|
|
|
884
1016
|
{ kind: 'sink', id: 'php-domxpath-query', language: 'php', framework: 'stdlib', match: { type: 'call', callee: 'query', receiver: '(?:xp|xpath|dom)' }, argIndex: 0,
|
|
885
1017
|
vuln: { name: 'XPath Injection (DOMXPath::query)', severity: 'high', cwe: 'CWE-643',
|
|
886
1018
|
remediation: 'Never concat user input into the expression string; validate/allow-list the queried attribute value instead.' } },
|
|
1019
|
+
// SimpleXMLElement::xpath() — PHP's OTHER, more commonly used XPath query
|
|
1020
|
+
// API (distinct from DOMXPath::query above), called on a SimpleXMLElement
|
|
1021
|
+
// object which can be named anything (`$xml->xpath($query)` — no `xp`/
|
|
1022
|
+
// `xpath`/`dom`-shaped receiver name to key off, unlike DOMXPath's own
|
|
1023
|
+
// idiomatic naming). No `match.receiver` set deliberately: `xpath` is a
|
|
1024
|
+
// specific enough bare method name that gating on receiver naming would
|
|
1025
|
+
// only lose recall, not gain precision. Tagged CWE-91 (not CWE-643) to
|
|
1026
|
+
// match this specific API/CWE pairing as filed by NIST SARD's own PHP
|
|
1027
|
+
// Vulnerability Test Suite (Stivalet & Delaitre) — confirmed from that
|
|
1028
|
+
// suite's PUBLIC generator source (github.com/stivalet/
|
|
1029
|
+
// php-vuln-test-suite-generator, execQuery_XPath.txt: `$xml =
|
|
1030
|
+
// simplexml_load_file(...); $res=$xml->xpath($query);`), not from reading
|
|
1031
|
+
// this project's own ingested/scored corpus copy.
|
|
1032
|
+
{ kind: 'sink', id: 'php-simplexml-xpath', language: 'php', framework: 'stdlib', match: { type: 'call', callee: 'xpath' }, argIndex: 0,
|
|
1033
|
+
vuln: { name: 'XPath Injection (SimpleXMLElement::xpath)', severity: 'high', cwe: 'CWE-91',
|
|
1034
|
+
remediation: 'Never concat user input into the expression string; validate/allow-list the queried attribute value instead.' } },
|
|
887
1035
|
{ kind: 'sink', id: 'go-htmlquery-find', language: 'go', framework: 'htmlquery', match: { type: 'call', callee: 'Find', receiver: 'htmlquery' }, argIndex: 1,
|
|
888
1036
|
vuln: { name: 'XPath Injection (htmlquery.Find)', severity: 'high', cwe: 'CWE-643',
|
|
889
1037
|
remediation: 'Never concat user input into the expression string; validate/allow-list the queried attribute value instead.' } },
|
|
@@ -974,8 +1122,60 @@ export const CATALOG = [
|
|
|
974
1122
|
{ kind: 'sanitizer', id: 'php-htmlentities', language: 'php', match: { type: 'call', callee: 'htmlentities' }, effect: 'strip', appliesTo: ['xss'] },
|
|
975
1123
|
{ kind: 'sanitizer', id: 'php-escapeshellarg', language: 'php', match: { type: 'call', callee: 'escapeshellarg' }, effect: 'strip', appliesTo: ['cmd'] },
|
|
976
1124
|
{ kind: 'sanitizer', id: 'php-escapeshellcmd', language: 'php', match: { type: 'call', callee: 'escapeshellcmd' }, effect: 'strip', appliesTo: ['cmd'] },
|
|
977
|
-
|
|
978
|
-
|
|
1125
|
+
// SARD PHP corpus, sanitizer-semantics precision pass. `effect: 'coerce'`
|
|
1126
|
+
// (distinct from `effect: 'strip'`) marks a value that becomes a genuinely
|
|
1127
|
+
// non-string type — an int/float can never carry a SQL/XSS/command-
|
|
1128
|
+
// injection payload regardless of family, which is why `appliesTo: ['*']`
|
|
1129
|
+
// is honest here in a way it is NOT for `htmlspecialchars`/
|
|
1130
|
+
// `escapeshellarg` above (those only neutralize ONE family's
|
|
1131
|
+
// metacharacters, appliesTo is scoped accordingly). `effect: 'coerce'`
|
|
1132
|
+
// pairs with dataflow/engine.js's `_isCoercionCall`, which actually KILLS
|
|
1133
|
+
// taint (not just demotes) for a universal-appliesTo sanitizer match.
|
|
1134
|
+
{ kind: 'sanitizer', id: 'php-intval', language: 'php', match: { type: 'call', callee: 'intval' }, effect: 'coerce', appliesTo: ['*'] },
|
|
1135
|
+
{ kind: 'sanitizer', id: 'php-floatval', language: 'php', match: { type: 'call', callee: 'floatval' }, effect: 'coerce', appliesTo: ['*'] },
|
|
1136
|
+
// filter_var($x, FILTER_VALIDATE_INT|FLOAT) genuinely coerces (returns the
|
|
1137
|
+
// typed value or `false`) — a real sanitizer. Every OTHER filter_var call
|
|
1138
|
+
// shape (no filter arg at all, defaulting to FILTER_DEFAULT which does
|
|
1139
|
+
// nothing; FILTER_SANITIZE_* filters, which strip characters but do not
|
|
1140
|
+
// guarantee the family-relevant metacharacter is gone; FILTER_VALIDATE_*
|
|
1141
|
+
// for anything other than INT/FLOAT) is NOT a sanitizer for any family
|
|
1142
|
+
// this catalog scores, and the corpus's own PHP `good` cases were found
|
|
1143
|
+
// (this session) to use `filter_var` with `FILTER_VALIDATE_INT`/`FLOAT`
|
|
1144
|
+
// specifically for its 288/288 genuinely-safe cases — a single blanket
|
|
1145
|
+
// `appliesTo: ['*']` entry (the previous version of this catalog) would
|
|
1146
|
+
// have silently marked EVERY filter_var call safe regardless of which
|
|
1147
|
+
// filter was actually passed, which is backwards for FILTER_SANITIZE_*
|
|
1148
|
+
// and simply wrong for the no-filter-arg default. `requireLiteralArg`
|
|
1149
|
+
// scopes the match to the two filters that are actually type coercions;
|
|
1150
|
+
// dataflow/engine.js's `_isCoercionCall` consults this same
|
|
1151
|
+
// `match.requireLiteralArg` gate (shared with the sink-side check via
|
|
1152
|
+
// `_literalArgSatisfied`), so a bare/wrong-filter `filter_var` call
|
|
1153
|
+
// correctly does not qualify as a coercion.
|
|
1154
|
+
{ kind: 'sanitizer', id: 'php-filter-var-int', language: 'php',
|
|
1155
|
+
match: { type: 'call', callee: 'filter_var', requireLiteralArg: { index: 1, pattern: '^FILTER_VALIDATE_INT$' } },
|
|
1156
|
+
effect: 'coerce', appliesTo: ['*'] },
|
|
1157
|
+
{ kind: 'sanitizer', id: 'php-filter-var-float', language: 'php',
|
|
1158
|
+
match: { type: 'call', callee: 'filter_var', requireLiteralArg: { index: 1, pattern: '^FILTER_VALIDATE_FLOAT$' } },
|
|
1159
|
+
effect: 'coerce', appliesTo: ['*'] },
|
|
1160
|
+
// settype($x, "integer"|"int"|"float"|"double") mutates $x in place to a
|
|
1161
|
+
// non-string type — same coercion argument as intval/floatval above, but
|
|
1162
|
+
// only when the TYPE argument is actually numeric (settype($x, "string")
|
|
1163
|
+
// or ("array") is the opposite of a sanitizer).
|
|
1164
|
+
{ kind: 'sanitizer', id: 'php-settype-numeric', language: 'php',
|
|
1165
|
+
match: { type: 'call', callee: 'settype', requireLiteralArg: { index: 1, pattern: '^"(?:int|integer|float|double)"$' } },
|
|
1166
|
+
effect: 'coerce', appliesTo: ['*'] },
|
|
1167
|
+
// Escaping functions, family-scoped and NOT universal: each only
|
|
1168
|
+
// neutralizes ITS OWN sink's metacharacter grammar. mysql(i)_real_escape_string
|
|
1169
|
+
// and pg_escape_string escape SQL string-literal delimiters specifically —
|
|
1170
|
+
// they do nothing for command/LDAP/XSS injection, and (a real, disclosed
|
|
1171
|
+
// limitation, not fixed here) they are only actually safe when the escaped
|
|
1172
|
+
// value is placed INSIDE a quoted SQL string literal, which this catalog
|
|
1173
|
+
// has no mechanism to verify.
|
|
1174
|
+
{ kind: 'sanitizer', id: 'php-mysqli-real-escape-string', language: 'php', match: { type: 'call', callee: 'real_escape_string' }, effect: 'strip', appliesTo: ['sql'] },
|
|
1175
|
+
{ kind: 'sanitizer', id: 'php-mysql-real-escape-string', language: 'php', match: { type: 'call', callee: 'mysql_real_escape_string' }, effect: 'strip', appliesTo: ['sql'] },
|
|
1176
|
+
{ kind: 'sanitizer', id: 'php-pg-escape-string', language: 'php', match: { type: 'call', callee: 'pg_escape_string' }, effect: 'strip', appliesTo: ['sql'] },
|
|
1177
|
+
{ kind: 'sanitizer', id: 'php-addslashes', language: 'php', match: { type: 'call', callee: 'addslashes' }, effect: 'strip', appliesTo: ['sql'] },
|
|
1178
|
+
{ kind: 'sanitizer', id: 'php-ldap-escape', language: 'php', match: { type: 'call', callee: 'ldap_escape' }, effect: 'strip', appliesTo: ['ldap'] },
|
|
979
1179
|
|
|
980
1180
|
// ─── SANITIZERS (Ruby) ────────────────────────────────────────────────────
|
|
981
1181
|
{ kind: 'sanitizer', id: 'rb-rails-html-escape', language: 'rb', match: { type: 'call', callee: 'h' }, effect: 'strip', appliesTo: ['xss'] },
|
|
@@ -1192,6 +1392,35 @@ export const CATALOG = [
|
|
|
1192
1392
|
{ kind: 'sink', id: 'java-servlet-setheader', language: 'java', framework: 'servlet', match: { type: 'call', callee: 'setHeader' }, argIndex: 1,
|
|
1193
1393
|
vuln: { name: 'HTTP Response Splitting / Header Injection (HttpServletResponse.setHeader)', severity: 'high', cwe: 'CWE-113',
|
|
1194
1394
|
remediation: 'Strip/validate CR/LF from any user-controlled value before using it as a header value.' } },
|
|
1395
|
+
{ kind: 'sink', id: 'java-servlet-addheader', language: 'java', framework: 'servlet', match: { type: 'call', callee: 'addHeader' }, argIndex: 1,
|
|
1396
|
+
vuln: { name: 'HTTP Response Splitting / Header Injection (HttpServletResponse.addHeader)', severity: 'high', cwe: 'CWE-113',
|
|
1397
|
+
remediation: 'Strip/validate CR/LF from any user-controlled value before using it as a header value.' } },
|
|
1398
|
+
// `Cookie`'s own value comes from its constructor's 2nd arg, so the sink
|
|
1399
|
+
// is the constructor, not addCookie() (which only ever receives the
|
|
1400
|
+
// already-built Cookie object).
|
|
1401
|
+
{ kind: 'sink', id: 'java-new-Cookie', language: 'java', framework: 'servlet', match: { type: 'call', callee: 'Cookie' }, argIndex: 1,
|
|
1402
|
+
vuln: { name: 'HTTP Response Splitting / Header Injection (new Cookie(name, value))', severity: 'high', cwe: 'CWE-113',
|
|
1403
|
+
remediation: 'Strip/validate CR/LF from any user-controlled value before using it as a cookie value.' } },
|
|
1404
|
+
// CWE-470: unsafe reflection. Juliet's canonical shape passes a
|
|
1405
|
+
// user-controlled class name straight to Class.forName, then constructs
|
|
1406
|
+
// or invokes it — the class-name argument is the attacker's real lever.
|
|
1407
|
+
{ kind: 'sink', id: 'java-class-forname', language: 'java', framework: 'stdlib', match: { type: 'call', callee: 'forName' }, argIndex: 0,
|
|
1408
|
+
vuln: { name: 'Unsafe Reflection (Class.forName)', severity: 'high', cwe: 'CWE-470',
|
|
1409
|
+
remediation: 'Validate the class name against an explicit allow-list before loading it reflectively.' } },
|
|
1410
|
+
{ kind: 'sink', id: 'java-method-invoke', language: 'java', framework: 'stdlib', match: { type: 'call', callee: 'invoke', receiver: '^method$' }, argIndex: 'all',
|
|
1411
|
+
vuln: { name: 'Unsafe Reflection (Method.invoke)', severity: 'high', cwe: 'CWE-470',
|
|
1412
|
+
remediation: 'Validate the target method/class against an explicit allow-list before invoking it reflectively.' } },
|
|
1413
|
+
// CWE-134: uncontrolled format string. `String.format`/`Formatter.format`/
|
|
1414
|
+
// `PrintStream.printf` all treat arg 0 as the format string itself; a
|
|
1415
|
+
// tainted format string (as opposed to a tainted format ARGUMENT, which is
|
|
1416
|
+
// ordinary and safe) can leak memory contents via `%s`/`%n$s` or crash the
|
|
1417
|
+
// process.
|
|
1418
|
+
{ kind: 'sink', id: 'java-string-format', language: 'java', framework: 'stdlib', match: { type: 'call', callee: 'format' }, argIndex: 0,
|
|
1419
|
+
vuln: { name: 'Uncontrolled Format String (String.format / Formatter.format)', severity: 'medium', cwe: 'CWE-134',
|
|
1420
|
+
remediation: 'Never pass user input as the format string itself; use a fixed format string and pass user input only as an argument.' } },
|
|
1421
|
+
{ kind: 'sink', id: 'java-printstream-printf', language: 'java', framework: 'stdlib', match: { type: 'call', callee: 'printf' }, argIndex: 0,
|
|
1422
|
+
vuln: { name: 'Uncontrolled Format String (PrintStream.printf)', severity: 'medium', cwe: 'CWE-134',
|
|
1423
|
+
remediation: 'Never pass user input as the format string itself; use a fixed format string and pass user input only as an argument.' } },
|
|
1195
1424
|
{ kind: 'sink', id: 'kt-servlet-setheader', language: 'kt', framework: 'servlet', match: { type: 'call', callee: 'setHeader' }, argIndex: 1,
|
|
1196
1425
|
vuln: { name: 'HTTP Response Splitting / Header Injection (HttpServletResponse.setHeader)', severity: 'high', cwe: 'CWE-113',
|
|
1197
1426
|
remediation: 'Strip/validate CR/LF from any user-controlled value before using it as a header value.' } },
|
|
@@ -1207,7 +1436,18 @@ export const CATALOG = [
|
|
|
1207
1436
|
{ kind: 'sanitizer', id: 'py-urllib-quote', language: 'py', match: { type: 'call', callee: 'quote_plus' }, effect: 'strip', appliesTo: ['url'] },
|
|
1208
1437
|
{ kind: 'sanitizer', id: 'py-int-v2', language: 'py', match: { type: 'call', callee: 'int' }, effect: 'strip', appliesTo: ['*'] },
|
|
1209
1438
|
{ kind: 'sanitizer', id: 'py-float-v2', language: 'py', match: { type: 'call', callee: 'float' }, effect: 'strip', appliesTo: ['*'] },
|
|
1210
|
-
|
|
1439
|
+
// NOT a type-coercion sanitizer despite the old appliesTo:['*'] tag:
|
|
1440
|
+
// ast.literal_eval("'; DROP TABLE users;--'") returns that exact STRING
|
|
1441
|
+
// unchanged — a Python string literal evaluates to itself. Its only real
|
|
1442
|
+
// safety property is preventing arbitrary CODE execution (the eval()
|
|
1443
|
+
// replacement it exists for), not neutralizing the resulting value's
|
|
1444
|
+
// content for any other downstream sink. `appliesTo:['*']` was safe only
|
|
1445
|
+
// as long as a sanitizer match merely DEMOTED confidence; now that
|
|
1446
|
+
// dataflow/engine.js's _isCoercionCall treats a universal tag as licence to
|
|
1447
|
+
// KILL taint outright, this entry would silently hide a real SQL/command
|
|
1448
|
+
// injection whose payload happens to parse as a Python literal. appliesTo
|
|
1449
|
+
// left empty (never matches any family) until this is scoped correctly.
|
|
1450
|
+
{ kind: 'sanitizer', id: 'py-ast-literal-eval', language: 'py', match: { type: 'call', callee: 'literal_eval' }, effect: 'strip', appliesTo: [] },
|
|
1211
1451
|
{ kind: 'sanitizer', id: 'py-yaml-safe-load', language: 'py', match: { type: 'call', callee: 'safe_load' }, effect: 'strip', appliesTo: ['deserial'] },
|
|
1212
1452
|
{ kind: 'sanitizer', id: 'py-pathlib-resolve', language: 'py', match: { type: 'call', callee: 'resolve' }, effect: 'taintIf-not-pinned', appliesTo: ['path'] },
|
|
1213
1453
|
{ kind: 'sanitizer', id: 'py-defusedxml', language: 'py', match: { type: 'call', callee: 'fromstring' }, effect: 'strip', appliesTo: ['xxe'] }, // when called from defusedxml namespace
|
|
@@ -1219,7 +1459,61 @@ export const CATALOG = [
|
|
|
1219
1459
|
{ kind: 'source', id: 'cs-request-headers', language: 'cs', framework: 'aspnet', match: { type: 'member', object: 'Request', prop: 'Headers' }, label: 'Request.Headers', provenance: 'header' },
|
|
1220
1460
|
{ kind: 'source', id: 'cs-request-params', language: 'cs', framework: 'aspnet', match: { type: 'member', object: 'Request', prop: 'Params' }, label: 'Request.Params' },
|
|
1221
1461
|
{ kind: 'source', id: 'cs-request-body', language: 'cs', framework: 'aspnet-core', match: { type: 'member', object: 'Request', prop: 'Body' }, label: 'Request.Body', provenance: 'http-body' },
|
|
1462
|
+
// SARD_80_F1: the entries above only match a receiver literally named
|
|
1463
|
+
// `Request` (the ASP.NET-Core-controller convention, `this.Request.…`
|
|
1464
|
+
// stripped to its bare member). Juliet's own dominant local-variable
|
|
1465
|
+
// naming (`req`/`request` params, exactly as JS's `req.query`/
|
|
1466
|
+
// `request.body` entries already cover) never matched at all — same
|
|
1467
|
+
// gap, same fix, mirrored per property.
|
|
1468
|
+
{ kind: 'source', id: 'cs-req-form', language: 'cs', framework: 'aspnet', match: { type: 'member', object: 'req', prop: 'Form' }, label: 'req.Form', provenance: 'http-body' },
|
|
1469
|
+
{ kind: 'source', id: 'cs-req-query', language: 'cs', framework: 'aspnet', match: { type: 'member', object: 'req', prop: 'QueryString' }, label: 'req.QueryString', provenance: 'url-param' },
|
|
1470
|
+
{ kind: 'source', id: 'cs-req-cookies', language: 'cs', framework: 'aspnet', match: { type: 'member', object: 'req', prop: 'Cookies' }, label: 'req.Cookies', provenance: 'cookie' },
|
|
1471
|
+
{ kind: 'source', id: 'cs-req-headers', language: 'cs', framework: 'aspnet', match: { type: 'member', object: 'req', prop: 'Headers' }, label: 'req.Headers', provenance: 'header' },
|
|
1472
|
+
{ kind: 'source', id: 'cs-req-params', language: 'cs', framework: 'aspnet', match: { type: 'member', object: 'req', prop: 'Params' }, label: 'req.Params', provenance: 'url-param' },
|
|
1473
|
+
{ kind: 'source', id: 'cs-request-lc-form', language: 'cs', framework: 'aspnet', match: { type: 'member', object: 'request', prop: 'Form' }, label: 'request.Form', provenance: 'http-body' },
|
|
1474
|
+
{ kind: 'source', id: 'cs-request-lc-query', language: 'cs', framework: 'aspnet', match: { type: 'member', object: 'request', prop: 'QueryString' }, label: 'request.QueryString', provenance: 'url-param' },
|
|
1475
|
+
{ kind: 'source', id: 'cs-request-lc-cookies', language: 'cs', framework: 'aspnet', match: { type: 'member', object: 'request', prop: 'Cookies' }, label: 'request.Cookies', provenance: 'cookie' },
|
|
1476
|
+
{ kind: 'source', id: 'cs-request-lc-headers', language: 'cs', framework: 'aspnet', match: { type: 'member', object: 'request', prop: 'Headers' }, label: 'request.Headers', provenance: 'header' },
|
|
1477
|
+
{ kind: 'source', id: 'cs-request-lc-params', language: 'cs', framework: 'aspnet', match: { type: 'member', object: 'request', prop: 'Params' }, label: 'request.Params', provenance: 'url-param' },
|
|
1222
1478
|
{ kind: 'source', id: 'cs-env-var', language: 'cs', framework: 'stdlib', match: { type: 'call', callee: 'GetEnvironmentVariable' }, label: 'Environment.GetEnvironmentVariable', provenance: 'env' },
|
|
1479
|
+
{ kind: 'source', id: 'cs-env-args', language: 'cs', framework: 'stdlib', match: { type: 'call', callee: 'GetCommandLineArgs' }, label: 'Environment.GetCommandLineArgs', provenance: 'env' },
|
|
1480
|
+
// Console/stream/socket console-style reads — Juliet's other dominant
|
|
1481
|
+
// source family (`Console.ReadLine()`, `IO.StreamReader/TextReader/
|
|
1482
|
+
// BinaryReader.ReadLine()/ReadToEnd()`). Bare (receiver-unscoped): the
|
|
1483
|
+
// last-segment index match already makes `Console.ReadLine()` and
|
|
1484
|
+
// `sr.ReadLine()` share this one entry, exactly the "receiver-agnostic"
|
|
1485
|
+
// shape `cs-directorysearcher-filter`'s own comment documents for reads.
|
|
1486
|
+
// No `provenance` field on the 6 entries below: 'io-read'/'database' are
|
|
1487
|
+
// not values src/lineage/source-registry.js's PROVENANCE_MAP recognizes
|
|
1488
|
+
// (found via that registry's own pinned coverage-count test after this
|
|
1489
|
+
// catalog change merged) — the established convention for exactly this
|
|
1490
|
+
// shape (a stream/console read; a JDBC/ADO result-set read) is to declare
|
|
1491
|
+
// NO provenance field at all and let source-registry.js's own
|
|
1492
|
+
// NO_PROVENANCE_OVERRIDES table classify by entry id instead, matching
|
|
1493
|
+
// java-io-readline (user-input) and java-resultset-getstring/-getobject
|
|
1494
|
+
// (database-read) exactly. Overrides added there, not invented here.
|
|
1495
|
+
{ kind: 'source', id: 'cs-console-readline', language: 'cs', framework: 'stdlib', match: { type: 'call', callee: 'ReadLine' }, label: 'Console/TextReader.ReadLine' },
|
|
1496
|
+
{ kind: 'source', id: 'cs-reader-readtoend', language: 'cs', framework: 'stdlib', match: { type: 'call', callee: 'ReadToEnd' }, label: 'TextReader.ReadToEnd' },
|
|
1497
|
+
{ kind: 'source', id: 'cs-console-read', language: 'cs', framework: 'stdlib', match: { type: 'call', callee: 'Read', receiver: '^Console$' }, label: 'Console.Read' },
|
|
1498
|
+
{ kind: 'source', id: 'cs-tcpclient-getstream', language: 'cs', framework: 'stdlib', match: { type: 'call', callee: 'GetStream' }, label: 'TcpClient/TcpListener.GetStream', provenance: 'network' },
|
|
1499
|
+
{ kind: 'source', id: 'cs-registry-getvalue', language: 'cs', framework: 'stdlib', match: { type: 'call', callee: 'GetValue', receiver: '[Rr]egistry' }, label: 'Registry(Key).GetValue', provenance: 'env' },
|
|
1500
|
+
// `receiverTypeIn` (SARD_80_F1 W4.C39): additive to the `receiver` name
|
|
1501
|
+
// regex, never a replacement — confirms a match via CHA-resolved declared
|
|
1502
|
+
// type when Juliet's own idiomatic short variable name (`dr`) fails the
|
|
1503
|
+
// name check outright (no "reader" substring). Same
|
|
1504
|
+
// DATAREADER_SOURCE_TYPES list already established for the SEPARATE
|
|
1505
|
+
// lexical structural tracker at posture/csharp-analysis.js (W4.C34).
|
|
1506
|
+
{ kind: 'source', id: 'cs-datareader-getstring', language: 'cs', framework: 'ado', match: { type: 'call', callee: 'GetString', receiver: '[Rr]eader', receiverTypeIn: ['^(?:Sql|OleDb|Odbc|MySql|Npgsql|Sqlite)DataReader$'] }, label: 'SqlDataReader.GetString' },
|
|
1507
|
+
{ kind: 'source', id: 'cs-datareader-getvalue', language: 'cs', framework: 'ado', match: { type: 'call', callee: 'GetValue', receiver: '[Rr]eader', receiverTypeIn: ['^(?:Sql|OleDb|Odbc|MySql|Npgsql|Sqlite)DataReader$'] }, label: 'SqlDataReader.GetValue' },
|
|
1508
|
+
{ kind: 'source', id: 'cs-datareader-getint32', language: 'cs', framework: 'ado', match: { type: 'call', callee: 'GetInt32', receiver: '[Rr]eader', receiverTypeIn: ['^(?:Sql|OleDb|Odbc|MySql|Npgsql|Sqlite)DataReader$'] }, label: 'SqlDataReader.GetInt32' },
|
|
1509
|
+
// File-read variants: the CONTENTS of a file are untrusted data (a
|
|
1510
|
+
// taint SOURCE) independent of whether the PATH argument to the same
|
|
1511
|
+
// call is itself tainted (`cs-file-readall` below still checks that,
|
|
1512
|
+
// on the same call, for the path-traversal family — a call can be both
|
|
1513
|
+
// a source and a sink argument at once, same as every other language's
|
|
1514
|
+
// `open(path).read()` shape in this catalog).
|
|
1515
|
+
{ kind: 'source', id: 'cs-file-readalltext-src', language: 'cs', framework: 'stdlib', match: { type: 'call', callee: 'ReadAllText', receiver: '^File$' }, label: 'File.ReadAllText', provenance: 'file-read' },
|
|
1516
|
+
{ kind: 'source', id: 'cs-file-readalllines-src', language: 'cs', framework: 'stdlib', match: { type: 'call', callee: 'ReadAllLines', receiver: '^File$' }, label: 'File.ReadAllLines', provenance: 'file-read' },
|
|
1223
1517
|
|
|
1224
1518
|
// ─── SINKS (C#) ──────────────────────────────────────────────────────────
|
|
1225
1519
|
{ kind: 'sink', id: 'cs-sqlcommand', language: 'cs', framework: 'ado', match: { type: 'call', callee: 'SqlCommand' }, argIndex: 0,
|
|
@@ -1248,6 +1542,33 @@ export const CATALOG = [
|
|
|
1248
1542
|
match: { type: 'call', callee: 'Start', requireLiteralArg: { index: 0, pattern: '^"cmd(?:\\.exe)?"$|^"(?:/bin/)?(?:sh|bash)"$' } }, argIndex: 'all',
|
|
1249
1543
|
vuln: { name: 'Command Injection (Process.Start string-form)', severity: 'critical', cwe: 'CWE-78',
|
|
1250
1544
|
remediation: 'Use ProcessStartInfo with separated FileName + Arguments; never pass /c with concat.' } },
|
|
1545
|
+
// The SINGLE-argument `Process.Start(commandString)` overload — Juliet's
|
|
1546
|
+
// OWN canonical C# CWE-78 shape (confirmed by reading the real, public
|
|
1547
|
+
// Juliet C# mirror this project's own SARD manifest pins:
|
|
1548
|
+
// `github.com/snyk-schmidtty/juliet-test-suite-csharp`,
|
|
1549
|
+
// `CWE78_OS_Command_Injection__Connect_tcp_01.cs`:
|
|
1550
|
+
// `Process.Start(osCommand + data)`, the interpreter-plus-arguments
|
|
1551
|
+
// ALREADY concatenated into ONE tainted string before the call).
|
|
1552
|
+
// Neither entry above can ever match this: `cs-process-start`'s
|
|
1553
|
+
// `requireLiteralArg` fails CLOSED because arg0 here is a CONCAT
|
|
1554
|
+
// expression, not a literal (there is no separate, provably-shell
|
|
1555
|
+
// literal to check — the whole point of this shape); `cs-process-
|
|
1556
|
+
// start-args` checks argIndex 1, which does not exist on a one-argument
|
|
1557
|
+
// call. Unlike the 2-argument array-execve form (filename, args[]),
|
|
1558
|
+
// there is no safe interpretation of this overload with a tainted
|
|
1559
|
+
// argument — the ENTIRE string IS the thing .NET treats as a shell-
|
|
1560
|
+
// dispatchable command line — so no `requireLiteralArg` gate applies
|
|
1561
|
+
// here at all. Scoped to the `Process` receiver (a real BCL class name,
|
|
1562
|
+
// not scrambled by --scramble-identifiers) to avoid over-matching an
|
|
1563
|
+
// unrelated `.Start()` call (a `Timer`/`Thread`/`Stopwatch`, none of
|
|
1564
|
+
// which take a string command argument at all, but bare `callee:
|
|
1565
|
+
// 'Start'` with no receiver constraint — as the two entries above
|
|
1566
|
+
// already do — would still be a real precision risk for a hypothetical
|
|
1567
|
+
// future scrambled receiver).
|
|
1568
|
+
{ kind: 'sink', id: 'cs-process-start-single', language: 'cs', framework: 'stdlib',
|
|
1569
|
+
match: { type: 'call', callee: 'Start', receiver: '^Process$' }, argIndex: 0,
|
|
1570
|
+
vuln: { name: 'Command Injection (Process.Start single-argument command string)', severity: 'critical', cwe: 'CWE-78',
|
|
1571
|
+
remediation: 'Use ProcessStartInfo with separated FileName + ArgumentList; never build one concatenated command-line string.' } },
|
|
1251
1572
|
// Companion to the entry above, for the NON-shell-literal filename case
|
|
1252
1573
|
// that entry deliberately excludes (`Process.Start("ping", tainted)`).
|
|
1253
1574
|
// This is a real, distinct risk, not a benchmark-only shape:
|
|
@@ -1296,10 +1617,75 @@ export const CATALOG = [
|
|
|
1296
1617
|
{ kind: 'sink', id: 'cs-localredirect', language: 'cs', framework: 'aspnet', match: { type: 'call', callee: 'LocalRedirect' }, argIndex: 0,
|
|
1297
1618
|
vuln: { name: 'Open Redirect (Controller.LocalRedirect)', severity: 'medium', cwe: 'CWE-601',
|
|
1298
1619
|
remediation: 'LocalRedirect rejects absolute URLs, but a crafted relative path can still redirect off-site via scheme-relative (//evil.com) input — validate the path.' } },
|
|
1299
|
-
|
|
1620
|
+
// SARD 80% F1 push — this project's own W4.C1 investigation logged
|
|
1621
|
+
// C# CWE-80/81/83 as a "total blackout... zero findings of ANY kind
|
|
1622
|
+
// across 1084 real files", assumed to need deep architectural work.
|
|
1623
|
+
// Root cause, confirmed via the public Juliet C# mirror this project's
|
|
1624
|
+
// manifest pins (`CWE80_XSS__CWE182_Web_Connect_tcp_01.cs`): the sink is
|
|
1625
|
+
// `resp.Write(...)`, where `resp` is a METHOD PARAMETER
|
|
1626
|
+
// (`Bad(HttpRequest req, HttpResponse resp)`) — never the literal
|
|
1627
|
+
// identifier `Response` this entry's `receiver: '^Response$'` required,
|
|
1628
|
+
// exact-match, with no alternation at all. `receiver` widened to accept
|
|
1629
|
+
// the overwhelmingly common real-world parameter names
|
|
1630
|
+
// (`resp`/`response`, either casing) and `receiverTypeIn` added as an
|
|
1631
|
+
// ADDITIVE fallback (same pattern as `cs-htmltextwriter-write` below —
|
|
1632
|
+
// `receiver` stays required, since `_receiverAllowed` matches ANY
|
|
1633
|
+
// receiver when neither `receiver` nor `receiverBase` is set), seeded
|
|
1634
|
+
// from `resp`'s declared parameter type (`fn.paramTypes`, confirmed via
|
|
1635
|
+
// direct IR dump to already capture `resp: "HttpResponse"` with no
|
|
1636
|
+
// parser changes needed).
|
|
1637
|
+
{ kind: 'sink', id: 'cs-response-write', language: 'cs', framework: 'aspnet', match: { type: 'call', callee: 'Write', receiver: '[Rr]esp(?:onse)?', receiverTypeIn: ['^HttpResponse$'] }, argIndex: 0,
|
|
1300
1638
|
vuln: { name: 'Reflected XSS (Response.Write)', severity: 'high', cwe: 'CWE-79',
|
|
1301
1639
|
remediation: 'HTML-encode with HttpUtility.HtmlEncode before writing user input to the response.' } },
|
|
1302
|
-
|
|
1640
|
+
// CWE-81 (XSS in Error Message) — a member-WRITE sink, not a call at
|
|
1641
|
+
// all: Juliet's real C# shape (confirmed via the public Juliet mirror
|
|
1642
|
+
// this project's manifest pins,
|
|
1643
|
+
// CWE81_XSS_Error_Message__Web_Connect_tcp_01.cs) sets
|
|
1644
|
+
// `resp.StatusDescription = "..." + data;` directly. This was a
|
|
1645
|
+
// genuinely MISSING sink (no catalog entry of any kind referenced
|
|
1646
|
+
// `StatusDescription`), not a receiver-name mismatch like
|
|
1647
|
+
// `cs-response-write`'s bug above — the two gaps happened to share a
|
|
1648
|
+
// CWE-80/81/83 investigation but have different root causes.
|
|
1649
|
+
// `match.object: '_any_'` required for any member-WRITE sink (see
|
|
1650
|
+
// dataflow/CLAUDE.md's gotcha); same receiver/receiverTypeIn shape as
|
|
1651
|
+
// `cs-response-write` for consistency and scramble-safety.
|
|
1652
|
+
{ kind: 'sink', id: 'cs-response-statusdescription', language: 'cs', framework: 'aspnet', match: { type: 'member', object: '_any_', prop: 'StatusDescription', receiver: '[Rr]esp(?:onse)?', receiverTypeIn: ['^HttpResponse$'] }, argIndex: 'rhs',
|
|
1653
|
+
vuln: { name: 'Reflected XSS (Response.StatusDescription assigned a tainted value)', severity: 'high', cwe: 'CWE-81',
|
|
1654
|
+
remediation: 'HTML-encode with HttpUtility.HtmlEncode before assigning user input to StatusDescription, or avoid echoing it in a custom error page.' } },
|
|
1655
|
+
// SARD_80_F1 W4: `HtmlTextWriter` (ASP.NET Web Forms' `Render(HtmlTextWriter
|
|
1656
|
+
// writer)` override, and its declared-local-variable form
|
|
1657
|
+
// `HtmlTextWriter writer = ...; writer.Write(x);`) is a named W4 target
|
|
1658
|
+
// (495 dev cases) with NO prior catalog coverage at all — only the
|
|
1659
|
+
// structural `csharp.js` detector's name-based heuristic
|
|
1660
|
+
// (`/Writer|writer|output/i.test(call.receiver)`) could ever catch it,
|
|
1661
|
+
// and only when the variable happens to be named with "writer"/"output"
|
|
1662
|
+
// in it. `receiverTypeIn` (additive, never replaces a name check) lets
|
|
1663
|
+
// this ALSO fire off `class-hierarchy.js`'s `typeOfVar`, populated from
|
|
1664
|
+
// either a declared-type local (`parser-cs.js`'s `declaredType`) or a
|
|
1665
|
+
// parameter's declared type — same mechanism proven for Java's
|
|
1666
|
+
// `PrintWriter` two-step gap (see `java-writer-*` above).
|
|
1667
|
+
//
|
|
1668
|
+
// `receiver` is still REQUIRED here, not optional: `_receiverAllowed`
|
|
1669
|
+
// (catalog.js) returns `true` UNCONDITIONALLY when an entry sets neither
|
|
1670
|
+
// `receiver` nor `receiverBase` — so a `receiverTypeIn`-only entry would
|
|
1671
|
+
// match `Write`/`WriteLine` on ANY receiver whatsoever (Console, a
|
|
1672
|
+
// StreamWriter, a logger, literally anything), not narrow the match at
|
|
1673
|
+
// all. Caught before shipping by re-reading `_receiverAllowed`'s own
|
|
1674
|
+
// short-circuit, not by a failing test — `Write`/`WriteLine` are far too
|
|
1675
|
+
// generic a method name for `receiverTypeIn` to safely stand alone the
|
|
1676
|
+
// way the single-purpose `getString`/`fromString`-style entries
|
|
1677
|
+
// elsewhere in this file do. The name pattern mirrors the pre-existing
|
|
1678
|
+
// structural detector's own convention (`csharp.js`'s
|
|
1679
|
+
// `/Writer|writer|output/i`) so the two stay aligned.
|
|
1680
|
+
{ kind: 'sink', id: 'cs-htmltextwriter-write', language: 'cs', framework: 'aspnet', match: { type: 'call', callee: 'Write', receiver: '(?:[Ww]riter|output)', receiverTypeIn: ['^HtmlTextWriter$'] }, argIndex: 0,
|
|
1681
|
+
vuln: { name: 'Reflected XSS (HtmlTextWriter.Write)', severity: 'high', cwe: 'CWE-79',
|
|
1682
|
+
remediation: 'HTML-encode with HttpUtility.HtmlEncode before writing user input to an HtmlTextWriter.' } },
|
|
1683
|
+
{ kind: 'sink', id: 'cs-htmltextwriter-writeline', language: 'cs', framework: 'aspnet', match: { type: 'call', callee: 'WriteLine', receiver: '(?:[Ww]riter|output)', receiverTypeIn: ['^HtmlTextWriter$'] }, argIndex: 0,
|
|
1684
|
+
vuln: { name: 'Reflected XSS (HtmlTextWriter.WriteLine)', severity: 'high', cwe: 'CWE-79',
|
|
1685
|
+
remediation: 'HTML-encode with HttpUtility.HtmlEncode before writing user input to an HtmlTextWriter.' } },
|
|
1686
|
+
// Same `resp` (parameter name, not the literal identifier `Response`)
|
|
1687
|
+
// root cause as `cs-response-write` above.
|
|
1688
|
+
{ kind: 'sink', id: 'cs-response-addheader', language: 'cs', framework: 'aspnet', match: { type: 'call', callee: 'AddHeader', receiver: '[Rr]esp(?:onse)?', receiverTypeIn: ['^HttpResponse$'] }, argIndex: 1,
|
|
1303
1689
|
vuln: { name: 'HTTP Response Splitting / Header Injection (Response.AddHeader)', severity: 'high', cwe: 'CWE-113',
|
|
1304
1690
|
remediation: 'Strip CR/LF from header values, or use a framework API that rejects them automatically.' } },
|
|
1305
1691
|
{ kind: 'sink', id: 'cs-xmldoc-load', language: 'cs', framework: 'stdlib', match: { type: 'call', callee: 'Load', receiver: '^(?:[Xx]ml[Dd]oc(?:ument)?|xmlDoc|doc)$' }, argIndex: 0,
|
|
@@ -1333,12 +1719,135 @@ export const CATALOG = [
|
|
|
1333
1719
|
vuln: { name: 'LDAP Injection (DirectorySearcher.Filter assigned a concatenated value)', severity: 'high', cwe: 'CWE-90',
|
|
1334
1720
|
remediation: 'Escape LDAP special characters in filter components, or build the filter with a parameterized helper.' } },
|
|
1335
1721
|
|
|
1722
|
+
// `CommandText = tainted` — Juliet's dominant real-world SQLi shape,
|
|
1723
|
+
// even more common than the `new SqlCommand(tainted, conn)` constructor
|
|
1724
|
+
// form `cs-sqlcommand` already covers: build the command with a fixed
|
|
1725
|
+
// (often parameterless) constructor, then assign the query text to the
|
|
1726
|
+
// property afterward. `match.object: '_any_'` is required for any
|
|
1727
|
+
// member-WRITE sink (see dataflow/CLAUDE.md's gotcha on this) — object
|
|
1728
|
+
// specificity is applied afterward via `receiverTypeIn`.
|
|
1729
|
+
{ kind: 'sink', id: 'cs-commandtext-write', language: 'cs', framework: 'ado', match: { type: 'member', object: '_any_', prop: 'CommandText', receiverTypeIn: ['^(?:Sql|OleDb|Odbc|MySql|Npgsql|Sqlite)Command$'] }, argIndex: 'rhs',
|
|
1730
|
+
vuln: { name: 'SQL Injection (CommandText assigned a concatenated/tainted value)', severity: 'critical', cwe: 'CWE-89',
|
|
1731
|
+
remediation: 'Use a parameterized query: keep `CommandText` a fixed string with `@name` placeholders and bind values via `cmd.Parameters.AddWithValue(...)`.' } },
|
|
1732
|
+
// Juliet's canonical C# CWE-78 shape: build a `Process` object with a
|
|
1733
|
+
// parameterless/fixed constructor, then assign the tainted command text
|
|
1734
|
+
// to `.StartInfo.Arguments` (or, less commonly, `.StartInfo.FileName`)
|
|
1735
|
+
// afterward — the exact same "construct-then-assign" idiom
|
|
1736
|
+
// `cs-commandtext-write` above already models for SQL, just never
|
|
1737
|
+
// ported to this sink. Found via direct probing (a synthetic
|
|
1738
|
+
// `Process.Start()`-based Juliet-shaped fixture produced ZERO findings
|
|
1739
|
+
// of ANY kind before this entry existed), not assumed: the two PRE-
|
|
1740
|
+
// EXISTING C# CWE-78 entries (`cs-process-start`/`cs-process-start-args`)
|
|
1741
|
+
// both key off the static `Process.Start(...)` CALL form, which this
|
|
1742
|
+
// idiom never uses at all — `.Start()` is called on the instance with NO
|
|
1743
|
+
// arguments, so neither entry's `argIndex` had anything to check.
|
|
1744
|
+
// `match.object: '_any_'` required for any member-WRITE sink (see
|
|
1745
|
+
// dataflow/CLAUDE.md's gotcha); scoped via a name-based `receiver`
|
|
1746
|
+
// rather than `receiverTypeIn` since `ProcessStartInfo`'s own property
|
|
1747
|
+
// name (`StartInfo`) is a reliable, idiomatic .NET naming convention on
|
|
1748
|
+
// its own, and seeding `typeOfVar` for `new Process()` var was not yet
|
|
1749
|
+
// verified to work for this shape.
|
|
1750
|
+
{ kind: 'sink', id: 'cs-processstartinfo-arguments', language: 'cs', framework: 'stdlib', match: { type: 'member', object: '_any_', prop: 'Arguments', receiver: 'StartInfo' }, argIndex: 'rhs',
|
|
1751
|
+
vuln: { name: 'Command Injection (ProcessStartInfo.Arguments assigned a concatenated/tainted value)', severity: 'critical', cwe: 'CWE-78',
|
|
1752
|
+
remediation: 'Use ProcessStartInfo.ArgumentList (never a single concatenated Arguments string), or validate/allow-list the argument content.' } },
|
|
1753
|
+
{ kind: 'sink', id: 'cs-processstartinfo-filename', language: 'cs', framework: 'stdlib', match: { type: 'member', object: '_any_', prop: 'FileName', receiver: 'StartInfo' }, argIndex: 'rhs',
|
|
1754
|
+
vuln: { name: 'Command Injection (ProcessStartInfo.FileName assigned a tainted value)', severity: 'critical', cwe: 'CWE-78',
|
|
1755
|
+
remediation: 'Resolve the executable path against an explicit allow-list before assigning it to ProcessStartInfo.FileName.' } },
|
|
1756
|
+
{ kind: 'sink', id: 'cs-sqldataadapter', language: 'cs', framework: 'ado', match: { type: 'call', callee: 'SqlDataAdapter' }, argIndex: 0,
|
|
1757
|
+
vuln: { name: 'SQL Injection (new SqlDataAdapter with concatenated query text)', severity: 'critical', cwe: 'CWE-89',
|
|
1758
|
+
remediation: 'Pass a parameterized SqlCommand to the adapter instead of a raw query string.' } },
|
|
1759
|
+
{ kind: 'sink', id: 'cs-oledbcommand', language: 'cs', framework: 'ado', match: { type: 'call', callee: 'OleDbCommand' }, argIndex: 0,
|
|
1760
|
+
vuln: { name: 'SQL Injection (new OleDbCommand with concatenated query text)', severity: 'critical', cwe: 'CWE-89',
|
|
1761
|
+
remediation: 'Use parameterized OleDbCommand: bind values via `cmd.Parameters.AddWithValue(...)` rather than concatenating into the command text.' } },
|
|
1762
|
+
{ kind: 'sink', id: 'cs-odbccommand', language: 'cs', framework: 'ado', match: { type: 'call', callee: 'OdbcCommand' }, argIndex: 0,
|
|
1763
|
+
vuln: { name: 'SQL Injection (new OdbcCommand with concatenated query text)', severity: 'critical', cwe: 'CWE-89',
|
|
1764
|
+
remediation: 'Use parameterized OdbcCommand: bind values via `cmd.Parameters.AddWithValue(...)` rather than concatenating into the command text.' } },
|
|
1765
|
+
// Reflection-based dynamic type loading (CWE-470, "Unsafe Reflection") —
|
|
1766
|
+
// resolving/instantiating a type by a tainted name lets the caller pick
|
|
1767
|
+
// ANY loadable type, which is exactly what CWE-502-adjacent deserializer
|
|
1768
|
+
// gadget chains need.
|
|
1769
|
+
{ kind: 'sink', id: 'cs-type-gettype', language: 'cs', framework: 'stdlib', match: { type: 'call', callee: 'GetType', receiver: '^Type$' }, argIndex: 0,
|
|
1770
|
+
vuln: { name: 'Unsafe Reflection (Type.GetType with tainted type name)', severity: 'high', cwe: 'CWE-470',
|
|
1771
|
+
remediation: 'Resolve against an explicit allow-list of known-safe type names before calling Type.GetType/Activator.CreateInstance.' } },
|
|
1772
|
+
{ kind: 'sink', id: 'cs-assembly-load', language: 'cs', framework: 'stdlib', match: { type: 'call', callee: 'Load', receiver: '^Assembly$' }, argIndex: 0,
|
|
1773
|
+
vuln: { name: 'Unsafe Reflection (Assembly.Load with tainted assembly name)', severity: 'high', cwe: 'CWE-470',
|
|
1774
|
+
remediation: 'Never load an assembly by a user-controlled name or path; resolve against an explicit allow-list.' } },
|
|
1775
|
+
{ kind: 'sink', id: 'cs-activator-createinstance', language: 'cs', framework: 'stdlib', match: { type: 'call', callee: 'CreateInstance', receiver: '^Activator$' }, argIndex: 'all',
|
|
1776
|
+
vuln: { name: 'Unsafe Reflection (Activator.CreateInstance with a tainted type)', severity: 'high', cwe: 'CWE-470',
|
|
1777
|
+
remediation: 'Resolve the target type against an explicit allow-list before instantiating it dynamically.' } },
|
|
1778
|
+
// CWE-134 — an attacker-controlled FORMAT string (not just an
|
|
1779
|
+
// interpolated argument) can inject its own `{n}`/`%` directives.
|
|
1780
|
+
// Scoped to arg0 ONLY, deliberately: `String.Format`'s substitution
|
|
1781
|
+
// arguments (index 1+) are DATA, not format directives, so a tainted
|
|
1782
|
+
// substitution argument reaching an otherwise-literal format string
|
|
1783
|
+
// (`String.Format("Hello, {0}!", name)`) is not this vulnerability —
|
|
1784
|
+
// only the FORMAT STRING itself being attacker-controlled is. A bare
|
|
1785
|
+
// `Console.WriteLine(tainted)` single-argument sink was deliberately
|
|
1786
|
+
// NOT added alongside this: with only one argument there is no `{n}`
|
|
1787
|
+
// substitution happening at all (that overload does not treat its sole
|
|
1788
|
+
// argument as a format string), so it would have no real CWE-134
|
|
1789
|
+
// shape to detect — confirmed by a nested-call precision fixture where
|
|
1790
|
+
// such a sink fired on a value that was already-formatted output text
|
|
1791
|
+
// wrapping a merely-tainted substitution argument.
|
|
1792
|
+
// `^[Ss]tring$`, not `^String$`: C#'s `string` keyword is a real,
|
|
1793
|
+
// interchangeable alias for `System.String` (`string.Format(...)` and
|
|
1794
|
+
// `String.Format(...)` compile to the IDENTICAL call), and Juliet's own
|
|
1795
|
+
// real C# CWE-134 corpus (confirmed via the public Juliet mirror this
|
|
1796
|
+
// project's SARD manifest pins) uses the lowercase form exclusively
|
|
1797
|
+
// (`Console.Write(string.Format(data))`) — the capitalized-only pattern
|
|
1798
|
+
// silently missed 100% of this corpus's own CWE-134 cases, confirmed by
|
|
1799
|
+
// a direct probe (`String.Format` fires via IR-TAINT; the byte-identical
|
|
1800
|
+
// `string.Format` produces zero IR-TAINT findings before this fix).
|
|
1801
|
+
{ kind: 'sink', id: 'cs-string-format', language: 'cs', framework: 'stdlib', match: { type: 'call', callee: 'Format', receiver: '^[Ss]tring$' }, argIndex: 0,
|
|
1802
|
+
vuln: { name: 'Externally-Controlled Format String (String.Format)', severity: 'medium', cwe: 'CWE-134',
|
|
1803
|
+
remediation: 'Never build the FORMAT string itself from user input; keep the format literal and pass user data only as substitution arguments.' } },
|
|
1804
|
+
// CWE-643 — XPath built from concatenated/tainted text.
|
|
1805
|
+
//
|
|
1806
|
+
// `receiverTypeIn` added: Juliet's OWN canonical C# CWE-643 shape
|
|
1807
|
+
// (confirmed via the public Juliet C# mirror this project's SARD
|
|
1808
|
+
// manifest pins, `CWE643_Xpath_Injection__Connect_tcp_01.cs`) names its
|
|
1809
|
+
// `XPathNavigator` variable `xPath` — matching neither `[Nn]av` nor
|
|
1810
|
+
// `[Nn]avigator`. Worse than a one-off naming miss: under
|
|
1811
|
+
// `--scramble-identifiers` (the ONLY mode this benchmark ever scores
|
|
1812
|
+
// from), EVERY local variable name becomes an opaque hash regardless of
|
|
1813
|
+
// what it was originally called, so a receiver match keyed purely on
|
|
1814
|
+
// NAME can never fire on this benchmark at all, independent of Juliet's
|
|
1815
|
+
// own naming choice. `receiverTypeIn` is additive (OR'd with the name
|
|
1816
|
+
// check, never replacing it — see `_receiverTypeConfirms`), seeded here
|
|
1817
|
+
// from the declared-type annotation on `XPathNavigator xPath =
|
|
1818
|
+
// inputXml.CreateNavigator();` (parser-cs.js's `declaredType` capture on
|
|
1819
|
+
// assign nodes, landed earlier this session for an unrelated task,
|
|
1820
|
+
// needed no changes to support this).
|
|
1821
|
+
{ kind: 'sink', id: 'cs-xpathnavigator-select', language: 'cs', framework: 'stdlib', match: { type: 'call', callee: 'Select', receiver: '[Nn]av(?:igator)?', receiverTypeIn: ['^XPathNavigator$'] }, argIndex: 0,
|
|
1822
|
+
vuln: { name: 'XPath Injection (XPathNavigator.Select with concatenated expression)', severity: 'high', cwe: 'CWE-643',
|
|
1823
|
+
remediation: 'Use XPath variables (`XsltArgumentList`/`XPathExpression.SetContext`) instead of concatenating user input into the expression text.' } },
|
|
1824
|
+
{ kind: 'sink', id: 'cs-xpathnavigator-evaluate', language: 'cs', framework: 'stdlib', match: { type: 'call', callee: 'Evaluate', receiver: '[Nn]av(?:igator)?', receiverTypeIn: ['^XPathNavigator$'] }, argIndex: 0,
|
|
1825
|
+
vuln: { name: 'XPath Injection (XPathNavigator.Evaluate with concatenated expression)', severity: 'high', cwe: 'CWE-643',
|
|
1826
|
+
remediation: 'Use XPath variables instead of concatenating user input into the expression text.' } },
|
|
1827
|
+
{ kind: 'sink', id: 'cs-selectsinglenode', language: 'cs', framework: 'stdlib', match: { type: 'call', callee: 'SelectSingleNode' }, argIndex: 0,
|
|
1828
|
+
vuln: { name: 'XPath Injection (XmlNode.SelectSingleNode with concatenated expression)', severity: 'high', cwe: 'CWE-643',
|
|
1829
|
+
remediation: 'Use XPath variables instead of concatenating user input into the expression text.' } },
|
|
1336
1830
|
// ─── SANITIZERS (C#) ─────────────────────────────────────────────────────
|
|
1337
1831
|
{ kind: 'sanitizer', id: 'cs-html-encode', language: 'cs', match: { type: 'call', callee: 'HtmlEncode' }, effect: 'strip', appliesTo: ['xss'] },
|
|
1338
1832
|
{ kind: 'sanitizer', id: 'cs-url-encode', language: 'cs', match: { type: 'call', callee: 'UrlEncode' }, effect: 'strip', appliesTo: ['url'] },
|
|
1339
1833
|
{ kind: 'sanitizer', id: 'cs-path-getfullpath',language: 'cs', match: { type: 'call', callee: 'GetFullPath' }, effect: 'taintIf-not-pinned', appliesTo: ['path'] },
|
|
1340
|
-
|
|
1341
|
-
|
|
1834
|
+
// Receiver-scoped to numeric types (SARD_80_F1_SCANNER_PRD.md): `Parse` and
|
|
1835
|
+
// `TryParse` are extremely common bare method names across .NET — `XDocument
|
|
1836
|
+
// .Parse`, `JObject.Parse`, `DateTime.Parse`, `Uri`-adjacent parsers, and any
|
|
1837
|
+
// custom type's own `Parse` all share the name, and NONE of them coerce to a
|
|
1838
|
+
// non-string-injectable value the way a numeric parse does (a parsed XML/JSON
|
|
1839
|
+
// document's text content round-trips the original characters right back out
|
|
1840
|
+
// through `.ToString()`). Unscoped, this entry was already a precision bug
|
|
1841
|
+
// under the old demote-only semantics; it becomes an active false-negative
|
|
1842
|
+
// risk now that a universal tag can KILL taint outright (engine.js's
|
|
1843
|
+
// _isCoercionCall) — `var doc = XDocument.Parse(tainted); sink(doc.Value);`
|
|
1844
|
+
// would otherwise be reported clean.
|
|
1845
|
+
{ kind: 'sanitizer', id: 'cs-int-parse', language: 'cs',
|
|
1846
|
+
match: { type: 'call', callee: 'Parse', receiver: '^[Uu]?(?:[Ii]nt(?:32|64|16)?|[Ll]ong|[Dd]ouble|[Ff]loat|[Dd]ecimal|[Ss]hort|[Bb]yte|SByte)$' },
|
|
1847
|
+
effect: 'strip', appliesTo: ['*'] },
|
|
1848
|
+
{ kind: 'sanitizer', id: 'cs-int-tryparse', language: 'cs',
|
|
1849
|
+
match: { type: 'call', callee: 'TryParse', receiver: '^[Uu]?(?:[Ii]nt(?:32|64|16)?|[Ll]ong|[Dd]ouble|[Ff]loat|[Dd]ecimal|[Ss]hort|[Bb]yte|SByte)$' },
|
|
1850
|
+
effect: 'strip', appliesTo: ['*'] },
|
|
1342
1851
|
{ kind: 'sanitizer', id: 'cs-regex-escape', language: 'cs', match: { type: 'call', callee: 'Escape' }, effect: 'strip', appliesTo: ['regex'] },
|
|
1343
1852
|
{ kind: 'sanitizer', id: 'cs-addwithvalue', language: 'cs', match: { type: 'call', callee: 'AddWithValue' }, effect: 'strip', appliesTo: ['sql'] },
|
|
1344
1853
|
|
|
@@ -1353,7 +1862,25 @@ export const CATALOG = [
|
|
|
1353
1862
|
{ kind: 'sink', id: 'kt-jdbc-execute', language: 'kt', framework: 'jdbc', match: { type: 'call', callee: 'executeQuery' }, argIndex: 0,
|
|
1354
1863
|
vuln: { name: 'SQL Injection (JDBC executeQuery from Kotlin)', severity: 'critical', cwe: 'CWE-89',
|
|
1355
1864
|
remediation: 'Use PreparedStatement + setX(N, v) — Kotlin string templates concatenated into SQL are still injection.' } },
|
|
1356
|
-
|
|
1865
|
+
// SARD_80_F1 W4.J26: `kt-exposed-exec` and `kt-runtime-exec` (below) both
|
|
1866
|
+
// key on the SAME bare callee name `exec` with argIndex 0 — the only real
|
|
1867
|
+
// JVM-runtime overload of that name being ambiguous between Exposed's SQL
|
|
1868
|
+
// `.exec(sql)` and `Runtime.exec(cmd)`. Since `java`/`kt` share one
|
|
1869
|
+
// catalog-scoping family (see `_LANG_FAMILY` above), this ALSO matched
|
|
1870
|
+
// Java's own `Runtime.getRuntime().exec(cmd)` idiom — confirmed on real
|
|
1871
|
+
// corpus data (`sard-juliet-java-strict`'s CWE-78 command-injection test
|
|
1872
|
+
// files): 251 dev-split false positives, ALL misreported as
|
|
1873
|
+
// "SQL Injection (Exposed.exec)" on a plain OS-command-execution call,
|
|
1874
|
+
// with the correct `kt-runtime-exec`/CWE-78 finding never surviving
|
|
1875
|
+
// `dedupeFindingsWithEvidence`'s same-severity tie-break (first-inserted
|
|
1876
|
+
// wins). `receiverExclude` (see below) is the ONLY negative-match form
|
|
1877
|
+
// this catalog supports (`py-compile`'s own precedent) — excludes any
|
|
1878
|
+
// call whose receiver chain contains `Runtime`, `getRuntime`, or
|
|
1879
|
+
// `ProcessBuilder` (Java's `Runtime`/`ProcessBuilder` classes are always
|
|
1880
|
+
// capitalized; a bare, receiver-less `exec(sql)` — Exposed's more common
|
|
1881
|
+
// shape — is untouched, since `receiverExclude` can never match a call
|
|
1882
|
+
// with no receiver segments at all).
|
|
1883
|
+
{ kind: 'sink', id: 'kt-exposed-exec', language: 'kt', framework: 'exposed', match: { type: 'call', callee: 'exec', receiverExclude: 'Runtime|ProcessBuilder' }, argIndex: 0,
|
|
1357
1884
|
vuln: { name: 'SQL Injection (Exposed.exec with raw string)', severity: 'critical', cwe: 'CWE-89',
|
|
1358
1885
|
remediation: 'Use Exposed DSL queries or named-parameter exec with a typed parameter list.' } },
|
|
1359
1886
|
{ kind: 'sink', id: 'kt-runtime-exec', language: 'kt', framework: 'stdlib', match: { type: 'call', callee: 'exec' }, argIndex: 0,
|
|
@@ -1393,6 +1920,16 @@ export const CATALOG = [
|
|
|
1393
1920
|
{ kind: 'source', id: 'cpp-read', language: 'cpp', framework: null, match: { type: 'call', callee: 'read' }, label: 'read()', provenance: 'file-read' },
|
|
1394
1921
|
{ kind: 'source', id: 'cpp-fread', language: 'cpp', framework: null, match: { type: 'call', callee: 'fread' }, label: 'fread()', provenance: 'file-read' },
|
|
1395
1922
|
{ kind: 'source', id: 'cpp-fgets', language: 'cpp', framework: null, match: { type: 'call', callee: 'fgets' }, label: 'fgets()', provenance: 'file-read' },
|
|
1923
|
+
// SARD PHP corpus: fgets()/fread() are receiver-agnostic in this
|
|
1924
|
+
// language too (`fgets($fh)` reads from stdin/a socket/a file alike).
|
|
1925
|
+
// Declared AFTER the cpp entries above on purpose: `matchSource`'s
|
|
1926
|
+
// no-file-context callers (test/cpp-integration.test.js's own
|
|
1927
|
+
// "non-colliding" assertion) tiebreak same-named cross-language entries by
|
|
1928
|
+
// declaration order when `_languageAllowed` has no file extension to
|
|
1929
|
+
// filter on; a real scan always has a file path, so `_languageAllowed`
|
|
1930
|
+
// picks the right language regardless of this order.
|
|
1931
|
+
{ kind: 'source', id: 'php-fgets', language: 'php', framework: 'core', match: { type: 'call', callee: 'fgets' }, label: 'fgets()', provenance: 'file-read' },
|
|
1932
|
+
{ kind: 'source', id: 'php-fread', language: 'php', framework: 'core', match: { type: 'call', callee: 'fread' }, label: 'fread()', provenance: 'file-read' },
|
|
1396
1933
|
{ kind: 'source', id: 'cpp-gets', language: 'cpp', framework: null, match: { type: 'call', callee: 'gets' }, label: 'gets()', provenance: 'stdin' },
|
|
1397
1934
|
{ kind: 'source', id: 'cpp-scanf', language: 'cpp', framework: null, match: { type: 'call', callee: 'scanf' }, label: 'scanf()', provenance: 'stdin' },
|
|
1398
1935
|
|
|
@@ -1426,6 +1963,210 @@ export const CATALOG = [
|
|
|
1426
1963
|
// inert, but a future sanitizer consumer would silently turn this into a
|
|
1427
1964
|
// false negative. realpath() stays: it genuinely canonicalises a path,
|
|
1428
1965
|
// which is the property CWE-22 sinks care about.
|
|
1966
|
+
|
|
1967
|
+
// ─── SOURCES (Rust) ───────────────────────────────────────────────────────
|
|
1968
|
+
// Rust's web frameworks (axum, actix-web, rocket, warp) deliver request data
|
|
1969
|
+
// through EXTRACTOR-TYPED parameters (`Query(q): Query<P>`, `q: web::Query<P>`),
|
|
1970
|
+
// not through calls on a request object, so the dominant source shape is the
|
|
1971
|
+
// annotation channel: `parser-rust.js` emits a `paramAnnotations` entry whose
|
|
1972
|
+
// `decorator` is the extractor's type name (or `RouteParam` for every plain
|
|
1973
|
+
// parameter of a `#[get("/…/<x>")]`-attributed rocket handler). A closure
|
|
1974
|
+
// handler's extractor param is lowered to a synthetic `__rs_extract__.<Type>`
|
|
1975
|
+
// call instead, matched by the receiver-scoped call sources below.
|
|
1976
|
+
{ kind: 'source', id: 'rs-extract-query', language: 'rs', framework: 'web', match: { type: 'annotation', name: 'Query' }, label: 'Query<T> extractor (axum/actix)', provenance: 'url-param' },
|
|
1977
|
+
{ kind: 'source', id: 'rs-extract-path', language: 'rs', framework: 'web', match: { type: 'annotation', name: 'Path' }, label: 'Path<T> extractor (axum/actix)', provenance: 'path-param' },
|
|
1978
|
+
{ kind: 'source', id: 'rs-extract-json', language: 'rs', framework: 'web', match: { type: 'annotation', name: 'Json' }, label: 'Json<T> extractor (axum/actix/rocket)', provenance: 'http-body' },
|
|
1979
|
+
{ kind: 'source', id: 'rs-extract-form', language: 'rs', framework: 'web', match: { type: 'annotation', name: 'Form' }, label: 'Form<T> extractor (axum/actix/rocket)', provenance: 'http-body' },
|
|
1980
|
+
{ kind: 'source', id: 'rs-extract-headermap', language: 'rs', framework: 'web', match: { type: 'annotation', name: 'HeaderMap' }, label: 'HeaderMap extractor (axum)', provenance: 'header' },
|
|
1981
|
+
{ kind: 'source', id: 'rs-extract-typedheader',language: 'rs', framework: 'web', match: { type: 'annotation', name: 'TypedHeader' }, label: 'TypedHeader<T> extractor (axum)', provenance: 'header' },
|
|
1982
|
+
{ kind: 'source', id: 'rs-extract-rawquery', language: 'rs', framework: 'web', match: { type: 'annotation', name: 'RawQuery' }, label: 'RawQuery extractor (axum)', provenance: 'url-param' },
|
|
1983
|
+
{ kind: 'source', id: 'rs-extract-rawform', language: 'rs', framework: 'web', match: { type: 'annotation', name: 'RawForm' }, label: 'RawForm extractor (axum)', provenance: 'http-body' },
|
|
1984
|
+
{ kind: 'source', id: 'rs-extract-multipart', language: 'rs', framework: 'web', match: { type: 'annotation', name: 'Multipart' }, label: 'Multipart extractor (axum/actix)', provenance: 'http-body' },
|
|
1985
|
+
{ kind: 'source', id: 'rs-extract-multipartform', language: 'rs', framework: 'web', match: { type: 'annotation', name: 'MultipartForm' }, label: 'MultipartForm extractor (actix)', provenance: 'http-body' },
|
|
1986
|
+
{ kind: 'source', id: 'rs-extract-httprequest',language: 'rs', framework: 'web', match: { type: 'annotation', name: 'HttpRequest' }, label: 'HttpRequest (actix)' },
|
|
1987
|
+
{ kind: 'source', id: 'rs-extract-request', language: 'rs', framework: 'web', match: { type: 'annotation', name: 'Request' }, label: 'Request<Body> (axum/hyper)' },
|
|
1988
|
+
{ kind: 'source', id: 'rs-extract-cookiejar', language: 'rs', framework: 'web', match: { type: 'annotation', name: 'CookieJar' }, label: 'CookieJar extractor', provenance: 'cookie' },
|
|
1989
|
+
{ kind: 'source', id: 'rs-extract-cookies', language: 'rs', framework: 'web', match: { type: 'annotation', name: 'Cookies' }, label: 'Cookies extractor', provenance: 'cookie' },
|
|
1990
|
+
{ kind: 'source', id: 'rs-extract-host', language: 'rs', framework: 'web', match: { type: 'annotation', name: 'Host' }, label: 'Host extractor (axum)', provenance: 'header' },
|
|
1991
|
+
{ kind: 'source', id: 'rs-extract-originaluri',language: 'rs', framework: 'web', match: { type: 'annotation', name: 'OriginalUri' }, label: 'OriginalUri extractor (axum)', provenance: 'url-param' },
|
|
1992
|
+
{ kind: 'source', id: 'rs-extract-rawpathparams', language: 'rs', framework: 'web', match: { type: 'annotation', name: 'RawPathParams' }, label: 'RawPathParams extractor (axum)', provenance: 'path-param' },
|
|
1993
|
+
{ kind: 'source', id: 'rs-extract-bytes', language: 'rs', framework: 'web', match: { type: 'annotation', name: 'Bytes' }, label: 'Bytes body extractor (axum)', provenance: 'http-body' },
|
|
1994
|
+
{ kind: 'source', id: 'rs-route-param', language: 'rs', framework: 'rocket', match: { type: 'annotation', name: 'RouteParam' }, label: 'route parameter of a #[get("/<x>")] handler (rocket)', provenance: 'path-param' },
|
|
1995
|
+
// Closure-handler extractors (`web::get().to(|q: web::Query<P>| async move { … })`).
|
|
1996
|
+
{ kind: 'source', id: 'rs-closure-extract-query', language: 'rs', framework: 'web', match: { type: 'call', callee: 'Query', receiver: '^__rs_extract__$' }, label: 'Query<T> closure extractor', provenance: 'url-param' },
|
|
1997
|
+
{ kind: 'source', id: 'rs-closure-extract-path', language: 'rs', framework: 'web', match: { type: 'call', callee: 'Path', receiver: '^__rs_extract__$' }, label: 'Path<T> closure extractor', provenance: 'path-param' },
|
|
1998
|
+
{ kind: 'source', id: 'rs-closure-extract-json', language: 'rs', framework: 'web', match: { type: 'call', callee: 'Json', receiver: '^__rs_extract__$' }, label: 'Json<T> closure extractor', provenance: 'http-body' },
|
|
1999
|
+
{ kind: 'source', id: 'rs-closure-extract-form', language: 'rs', framework: 'web', match: { type: 'call', callee: 'Form', receiver: '^__rs_extract__$' }, label: 'Form<T> closure extractor', provenance: 'http-body' },
|
|
2000
|
+
{ kind: 'source', id: 'rs-closure-extract-headers', language: 'rs', framework: 'web', match: { type: 'call', callee: 'HeaderMap', receiver: '^__rs_extract__$' }, label: 'HeaderMap closure extractor', provenance: 'header' },
|
|
2001
|
+
{ kind: 'source', id: 'rs-closure-extract-request', language: 'rs', framework: 'web', match: { type: 'call', callee: 'HttpRequest', receiver: '^__rs_extract__$' }, label: 'HttpRequest closure extractor' },
|
|
2002
|
+
// Request-object accessors (actix `HttpRequest`, hyper/axum `Request`).
|
|
2003
|
+
{ kind: 'source', id: 'rs-req-query-string', language: 'rs', framework: 'actix', match: { type: 'call', callee: 'query_string' }, label: 'req.query_string() (actix)', provenance: 'url-param' },
|
|
2004
|
+
{ kind: 'source', id: 'rs-req-match-info', language: 'rs', framework: 'actix', match: { type: 'call', callee: 'match_info' }, label: 'req.match_info() (actix)', provenance: 'path-param' },
|
|
2005
|
+
{ kind: 'source', id: 'rs-req-cookie', language: 'rs', framework: 'actix', match: { type: 'call', callee: 'cookie', receiver: '^(?:req|request|jar|cookies)$' }, label: 'req.cookie() (actix)', provenance: 'cookie' },
|
|
2006
|
+
{ kind: 'source', id: 'rs-req-headers', language: 'rs', framework: 'web', match: { type: 'call', callee: 'headers', receiver: '^(?:req|request|parts)$' }, label: 'req.headers()', provenance: 'header' },
|
|
2007
|
+
{ kind: 'source', id: 'rs-req-uri-query', language: 'rs', framework: 'hyper', match: { type: 'call', callee: 'query', receiver: '^uri$' }, label: 'req.uri().query() (hyper/axum)', provenance: 'url-param' },
|
|
2008
|
+
// warp composes filters; the extracted value arrives as a closure parameter
|
|
2009
|
+
// bound (by parser-rust.js) to the receiver chain, so `warp.query`-style
|
|
2010
|
+
// MEMBER sources cover `warp::query().map(|q| …)` and the CALL forms cover
|
|
2011
|
+
// `let q = warp::query::<P>();`.
|
|
2012
|
+
{ kind: 'source', id: 'rs-warp-query-member', language: 'rs', framework: 'warp', match: { type: 'member', object: 'warp', prop: 'query' }, label: 'warp::query() (warp)', provenance: 'url-param' },
|
|
2013
|
+
{ kind: 'source', id: 'rs-warp-body-member', language: 'rs', framework: 'warp', match: { type: 'member', object: 'warp', prop: 'body' }, label: 'warp::body::* (warp)', provenance: 'http-body' },
|
|
2014
|
+
{ kind: 'source', id: 'rs-warp-header-member', language: 'rs', framework: 'warp', match: { type: 'member', object: 'warp', prop: 'header' }, label: 'warp::header() (warp)', provenance: 'header' },
|
|
2015
|
+
{ kind: 'source', id: 'rs-warp-path-member', language: 'rs', framework: 'warp', match: { type: 'member', object: 'warp', prop: 'path' }, label: 'warp::path::param() (warp)', provenance: 'path-param' },
|
|
2016
|
+
{ kind: 'source', id: 'rs-warp-query', language: 'rs', framework: 'warp', match: { type: 'call', callee: 'query', receiver: '^warp$' }, label: 'warp::query() (warp)', provenance: 'url-param' },
|
|
2017
|
+
{ kind: 'source', id: 'rs-warp-param', language: 'rs', framework: 'warp', match: { type: 'call', callee: 'param', receiver: '^(?:warp|path)$' }, label: 'warp::path::param() (warp)', provenance: 'path-param' },
|
|
2018
|
+
{ kind: 'source', id: 'rs-warp-header', language: 'rs', framework: 'warp', match: { type: 'call', callee: 'header', receiver: '^warp$' }, label: 'warp::header() (warp)', provenance: 'header' },
|
|
2019
|
+
// Process environment and CLI arguments: untrusted in the same sense the
|
|
2020
|
+
// other languages' `getenv`/`argv` entries are.
|
|
2021
|
+
{ kind: 'source', id: 'rs-env-var', language: 'rs', framework: 'std', match: { type: 'call', callee: 'var', receiver: '^env$' }, label: 'std::env::var()', provenance: 'env' },
|
|
2022
|
+
{ kind: 'source', id: 'rs-env-var-os', language: 'rs', framework: 'std', match: { type: 'call', callee: 'var_os', receiver: '^env$' }, label: 'std::env::var_os()', provenance: 'env' },
|
|
2023
|
+
{ kind: 'source', id: 'rs-env-args', language: 'rs', framework: 'std', match: { type: 'call', callee: 'args', receiver: '^env$' }, label: 'std::env::args()', provenance: 'cli' },
|
|
2024
|
+
{ kind: 'source', id: 'rs-env-args-os',language: 'rs', framework: 'std', match: { type: 'call', callee: 'args_os', receiver: '^env$' }, label: 'std::env::args_os()', provenance: 'cli' },
|
|
2025
|
+
|
|
2026
|
+
// ─── SINKS (Rust) ─────────────────────────────────────────────────────────
|
|
2027
|
+
// parser-rust.js accumulates chain arguments INNERMOST-first (source order),
|
|
2028
|
+
// so on a builder chain like `sqlx::query(&sql).bind(x).fetch_all(pool)` the
|
|
2029
|
+
// SQL text is arg 0 whichever terminal method the chain ends on, and on
|
|
2030
|
+
// `Command::new("sh").arg("-c").arg(x).output()` the literal program name is
|
|
2031
|
+
// arg 0. Terminal-method entries below are receiver-scoped to the chain root.
|
|
2032
|
+
// `sqlx::query!(…)` keeps its `!` in the callee (`sqlx.query!`) and is never
|
|
2033
|
+
// matched: it is the compile-time-checked, parameterized form.
|
|
2034
|
+
{ kind: 'sink', id: 'rs-sql-query', language: 'rs', framework: 'sqlx/postgres/mysql/rusqlite', match: { type: 'call', callee: 'query', receiverExclude: '^(?:warp|uri|url|Url|Uri)$' }, argIndex: 0,
|
|
2035
|
+
vuln: { name: 'SQL Injection (query with runtime-built SQL)', severity: 'critical', cwe: 'CWE-89',
|
|
2036
|
+
remediation: 'Keep the SQL text a literal and bind the value: `sqlx::query("SELECT * FROM t WHERE id = $1").bind(id)`, `client.query("… WHERE id = $1", &[&id])`, or `conn.query_row("… WHERE id = ?1", params![id], …)`. Never build SQL with format!/concat from request data.' } },
|
|
2037
|
+
{ kind: 'sink', id: 'rs-sql-query-as', language: 'rs', framework: 'sqlx', match: { type: 'call', callee: 'query_as' }, argIndex: 0,
|
|
2038
|
+
vuln: { name: 'SQL Injection (sqlx::query_as with runtime-built SQL)', severity: 'critical', cwe: 'CWE-89',
|
|
2039
|
+
remediation: 'Use `sqlx::query_as::<_, T>("… WHERE id = $1").bind(id)` or the compile-checked `sqlx::query_as!(T, "… $1", id)`.' } },
|
|
2040
|
+
{ kind: 'sink', id: 'rs-sql-query-scalar', language: 'rs', framework: 'sqlx', match: { type: 'call', callee: 'query_scalar' }, argIndex: 0,
|
|
2041
|
+
vuln: { name: 'SQL Injection (sqlx::query_scalar with runtime-built SQL)', severity: 'critical', cwe: 'CWE-89',
|
|
2042
|
+
remediation: 'Use `sqlx::query_scalar("… WHERE id = $1").bind(id)` or `sqlx::query_scalar!`.' } },
|
|
2043
|
+
{ kind: 'sink', id: 'rs-sql-raw-sql', language: 'rs', framework: 'sqlx', match: { type: 'call', callee: 'raw_sql' }, argIndex: 0,
|
|
2044
|
+
vuln: { name: 'SQL Injection (sqlx::raw_sql)', severity: 'critical', cwe: 'CWE-89',
|
|
2045
|
+
remediation: '`raw_sql` cannot bind parameters; switch to `sqlx::query("…$1").bind(value)` for anything derived from input.' } },
|
|
2046
|
+
{ kind: 'sink', id: 'rs-sql-diesel-sql-query', language: 'rs', framework: 'diesel', match: { type: 'call', callee: 'sql_query' }, argIndex: 0,
|
|
2047
|
+
vuln: { name: 'SQL Injection (diesel::sql_query with runtime-built SQL)', severity: 'critical', cwe: 'CWE-89',
|
|
2048
|
+
remediation: 'Bind with `diesel::sql_query("… WHERE id = $1").bind::<Integer, _>(id)`, or use the typed query DSL instead of raw SQL.' } },
|
|
2049
|
+
{ kind: 'sink', id: 'rs-sql-diesel-sql', language: 'rs', framework: 'diesel', match: { type: 'call', callee: 'sql', receiver: '^(?:diesel|dsl)$' }, argIndex: 0,
|
|
2050
|
+
vuln: { name: 'SQL Injection (diesel::dsl::sql fragment from input)', severity: 'critical', cwe: 'CWE-89',
|
|
2051
|
+
remediation: 'A `sql()` fragment is spliced verbatim; keep it a literal and pass values via `.bind()` or the typed DSL.' } },
|
|
2052
|
+
{ kind: 'sink', id: 'rs-sql-execute', language: 'rs', framework: 'rusqlite/postgres/mysql', match: { type: 'call', callee: 'execute', receiverExclude: '^(?:Command|cmd|command|process)$' }, argIndex: 0,
|
|
2053
|
+
vuln: { name: 'SQL Injection (execute with runtime-built SQL)', severity: 'critical', cwe: 'CWE-89',
|
|
2054
|
+
remediation: 'Keep the statement a literal and bind: `conn.execute("INSERT INTO t (a) VALUES (?1)", params![a])` / `client.execute("… $1", &[&a])`.' } },
|
|
2055
|
+
{ kind: 'sink', id: 'rs-sql-execute-batch',language: 'rs', framework: 'rusqlite', match: { type: 'call', callee: 'execute_batch' }, argIndex: 0,
|
|
2056
|
+
vuln: { name: 'SQL Injection (execute_batch with runtime-built SQL)', severity: 'critical', cwe: 'CWE-89',
|
|
2057
|
+
remediation: '`execute_batch` runs unparameterized SQL; split into single `execute` calls with `params![…]` for any value from input.' } },
|
|
2058
|
+
{ kind: 'sink', id: 'rs-sql-batch-execute',language: 'rs', framework: 'postgres', match: { type: 'call', callee: 'batch_execute' }, argIndex: 0,
|
|
2059
|
+
vuln: { name: 'SQL Injection (batch_execute with runtime-built SQL)', severity: 'critical', cwe: 'CWE-89',
|
|
2060
|
+
remediation: '`batch_execute` takes no parameters; use `client.execute("… $1", &[&value])` for input-derived values.' } },
|
|
2061
|
+
{ kind: 'sink', id: 'rs-sql-prepare', language: 'rs', framework: 'rusqlite/postgres', match: { type: 'call', callee: 'prepare' }, argIndex: 0,
|
|
2062
|
+
vuln: { name: 'SQL Injection (prepare with runtime-built SQL)', severity: 'critical', cwe: 'CWE-89',
|
|
2063
|
+
remediation: 'Preparing a statement does not parameterize text that was already interpolated. Use `?1`/`$1` placeholders in a literal and pass values at execution time.' } },
|
|
2064
|
+
{ kind: 'sink', id: 'rs-sql-prepare-cached',language: 'rs', framework: 'rusqlite', match: { type: 'call', callee: 'prepare_cached' }, argIndex: 0,
|
|
2065
|
+
vuln: { name: 'SQL Injection (prepare_cached with runtime-built SQL)', severity: 'critical', cwe: 'CWE-89',
|
|
2066
|
+
remediation: 'Use `?1` placeholders in a literal statement and pass values via `params![…]`.' } },
|
|
2067
|
+
{ kind: 'sink', id: 'rs-sql-query-row', language: 'rs', framework: 'rusqlite', match: { type: 'call', callee: 'query_row' }, argIndex: 0,
|
|
2068
|
+
vuln: { name: 'SQL Injection (query_row with runtime-built SQL)', severity: 'critical', cwe: 'CWE-89',
|
|
2069
|
+
remediation: 'Use `conn.query_row("SELECT … WHERE id = ?1", params![id], |r| …)`.' } },
|
|
2070
|
+
{ kind: 'sink', id: 'rs-sql-query-map', language: 'rs', framework: 'rusqlite', match: { type: 'call', callee: 'query_map', receiver: '^(?:conn|db|connection|stmt)$' }, argIndex: 0,
|
|
2071
|
+
vuln: { name: 'SQL Injection (query_map with runtime-built SQL)', severity: 'critical', cwe: 'CWE-89',
|
|
2072
|
+
remediation: 'Prepare a literal statement with `?1` placeholders and pass values via `params![…]`.' } },
|
|
2073
|
+
...['fetch_all', 'fetch_one', 'fetch_optional', 'fetch', 'fetch_many', 'bind', 'persistent'].map(method => (
|
|
2074
|
+
{ kind: 'sink', id: `rs-sqlx-chain-${method}`, language: 'rs', framework: 'sqlx', match: { type: 'call', callee: method, receiver: '^(?:query|query_as|query_scalar|raw_sql|sql_query)$' }, argIndex: 0,
|
|
2075
|
+
vuln: { name: `SQL Injection (sqlx query chain .${method} with runtime-built SQL)`, severity: 'critical', cwe: 'CWE-89',
|
|
2076
|
+
remediation: 'Keep the SQL literal and bind values: `sqlx::query("SELECT * FROM t WHERE id = $1").bind(id).fetch_all(pool)`; only the placeholder form is parameterized.' } }
|
|
2077
|
+
)),
|
|
2078
|
+
// Command injection. `Command::new(<tainted>)` is always dangerous (the
|
|
2079
|
+
// program itself comes from input). Everything else is only shell-interpreted
|
|
2080
|
+
// when the program is a literal shell (`sh -c`), gated by requireLiteralArg on
|
|
2081
|
+
// arg 0 (the innermost `Command::new` argument); `Command::new("ls").arg(x)`
|
|
2082
|
+
// never invokes a shell and does not fire.
|
|
2083
|
+
{ kind: 'sink', id: 'rs-command-new-program', language: 'rs', framework: 'std::process', match: { type: 'call', callee: 'new', receiver: '^Command$' }, argIndex: 0,
|
|
2084
|
+
vuln: { name: 'Command Injection (Command::new with input-derived program)', severity: 'high', cwe: 'CWE-78',
|
|
2085
|
+
remediation: 'Resolve the program from a fixed allow-list (`match name { "ls" => "/bin/ls", … }`) instead of passing request data to `Command::new`.' } },
|
|
2086
|
+
...['arg', 'args', 'output', 'status', 'spawn', 'env', 'current_dir', 'stdin', 'stdout', 'stderr'].map(method => (
|
|
2087
|
+
{ kind: 'sink', id: `rs-command-shell-${method}`, language: 'rs', framework: 'std::process',
|
|
2088
|
+
match: { type: 'call', callee: method, receiver: '^Command$', requireLiteralArg: { index: 0, pattern: '^"(?:/bin/|/usr/bin/)?(?:sh|bash|zsh|dash|ksh)"$|^"cmd(?:\\.exe)?"$|^"powershell(?:\\.exe)?"$|^"pwsh"$' } }, argIndex: 'all',
|
|
2089
|
+
vuln: { name: 'Command Injection (Command::new("sh").arg("-c") with input-derived arguments)', severity: 'critical', cwe: 'CWE-78',
|
|
2090
|
+
remediation: 'Drop the shell: `Command::new("ping").arg("-c").arg("1").arg(&host)` passes argv directly and never interprets metacharacters. If a shell is unavoidable, allow-list the value or quote it with `shell_escape::escape`.' } }
|
|
2091
|
+
)),
|
|
2092
|
+
// Path traversal: std::fs / tokio::fs / async_std::fs free functions, and
|
|
2093
|
+
// File / OpenOptions constructors. `Path::new(x).join(y)` is not itself a
|
|
2094
|
+
// sink; the access is. A canonicalize + starts_with containment check in the
|
|
2095
|
+
// sink window is recognized by engine.js's dropGuardedFindings.
|
|
2096
|
+
...['read', 'read_to_string', 'read_dir', 'write', 'remove_file', 'remove_dir', 'remove_dir_all', 'create_dir', 'create_dir_all', 'copy', 'rename', 'hard_link', 'symlink', 'metadata', 'read_link', 'set_permissions'].map(method => (
|
|
2097
|
+
{ kind: 'sink', id: `rs-fs-${method}`, language: 'rs', framework: 'std::fs', match: { type: 'call', callee: method, receiver: '^fs$' }, argIndex: 'all',
|
|
2098
|
+
vuln: { name: `Path Traversal (fs::${method} with input-derived path)`, severity: 'high', cwe: 'CWE-22',
|
|
2099
|
+
remediation: 'Join onto a fixed base, canonicalize, and reject anything outside it: `let p = base.join(name).canonicalize()?; if !p.starts_with(&base) { return Err(…) }`. Reject `..` and absolute paths before use.' } }
|
|
2100
|
+
)),
|
|
2101
|
+
...['open', 'create', 'create_new', 'options'].map(method => (
|
|
2102
|
+
{ kind: 'sink', id: `rs-file-${method}`, language: 'rs', framework: 'std::fs', match: { type: 'call', callee: method, receiver: '^(?:File|OpenOptions)$' }, argIndex: 0,
|
|
2103
|
+
vuln: { name: `Path Traversal (File::${method} with input-derived path)`, severity: 'high', cwe: 'CWE-22',
|
|
2104
|
+
remediation: 'Canonicalize the joined path and verify `starts_with(base)` before opening; or map the input to a fixed set of known file names.' } }
|
|
2105
|
+
)),
|
|
2106
|
+
// Reflected XSS: an HTML body built from input. `HttpResponse::Ok().body(x)`
|
|
2107
|
+
// (actix), `Html(x)` (axum), `RawHtml(x)` (rocket), `warp::reply::html(x)`.
|
|
2108
|
+
{ kind: 'sink', id: 'rs-actix-body', language: 'rs', framework: 'actix', match: { type: 'call', callee: 'body', receiver: '^(?:HttpResponse|HttpResponseBuilder|Response)$' }, argIndex: 'all',
|
|
2109
|
+
vuln: { name: 'Cross-Site Scripting (response body built from input)', severity: 'high', cwe: 'CWE-79',
|
|
2110
|
+
remediation: 'Escape before interpolating: `html_escape::encode_text(&name)`, or render through an auto-escaping template engine (askama, tera) instead of `format!` into the body.' } },
|
|
2111
|
+
{ kind: 'sink', id: 'rs-axum-html', language: 'rs', framework: 'axum', match: { type: 'call', callee: 'Html', receiverExclude: '^__rs_extract__$' }, argIndex: 0,
|
|
2112
|
+
vuln: { name: 'Cross-Site Scripting (axum Html response built from input)', severity: 'high', cwe: 'CWE-79',
|
|
2113
|
+
remediation: '`Html(format!(…))` sends the string verbatim. Escape with `html_escape::encode_text` or return an askama/maud template, which escapes by default.' } },
|
|
2114
|
+
{ kind: 'sink', id: 'rs-rocket-rawhtml', language: 'rs', framework: 'rocket', match: { type: 'call', callee: 'RawHtml' }, argIndex: 0,
|
|
2115
|
+
vuln: { name: 'Cross-Site Scripting (rocket RawHtml response built from input)', severity: 'high', cwe: 'CWE-79',
|
|
2116
|
+
remediation: 'Return a `Template::render` (auto-escaped) or escape the value with `html_escape::encode_text` before wrapping it in `RawHtml`.' } },
|
|
2117
|
+
{ kind: 'sink', id: 'rs-warp-reply-html', language: 'rs', framework: 'warp', match: { type: 'call', callee: 'html', receiver: '^reply$' }, argIndex: 0,
|
|
2118
|
+
vuln: { name: 'Cross-Site Scripting (warp::reply::html built from input)', severity: 'high', cwe: 'CWE-79',
|
|
2119
|
+
remediation: 'Escape with `html_escape::encode_text` or render an auto-escaping template before `warp::reply::html`.' } },
|
|
2120
|
+
// SSRF: outbound HTTP clients given an input-derived URL.
|
|
2121
|
+
...['get', 'post', 'put', 'patch', 'delete', 'head', 'request'].map(method => (
|
|
2122
|
+
{ kind: 'sink', id: `rs-http-client-${method}`, language: 'rs', framework: 'reqwest/ureq', match: { type: 'call', callee: method, receiver: '^(?:reqwest|ureq|isahc|surf|Client|client|http_client|http)$' }, argIndex: 'all',
|
|
2123
|
+
vuln: { name: `Server-Side Request Forgery (HTTP client ${method} with input-derived URL)`, severity: 'high', cwe: 'CWE-918',
|
|
2124
|
+
remediation: 'Parse with `url::Url::parse`, then allow-list the scheme and host (and reject loopback/RFC1918/169.254.169.254) before the request is sent.' } }
|
|
2125
|
+
)),
|
|
2126
|
+
// Open redirect.
|
|
2127
|
+
...['to', 'temporary', 'permanent', 'see_other', 'found', 'moved'].map(method => (
|
|
2128
|
+
{ kind: 'sink', id: `rs-redirect-${method}`, language: 'rs', framework: 'web', match: { type: 'call', callee: method, receiver: '^Redirect$' }, argIndex: 0,
|
|
2129
|
+
vuln: { name: `Open Redirect (Redirect::${method} with input-derived target)`, severity: 'medium', cwe: 'CWE-601',
|
|
2130
|
+
remediation: 'Only redirect to relative paths you validate (`target.starts_with(\'/\') && !target.starts_with("//")`) or to hosts on an allow-list.' } }
|
|
2131
|
+
)),
|
|
2132
|
+
{ kind: 'sink', id: 'rs-actix-insert-header', language: 'rs', framework: 'actix', match: { type: 'call', callee: 'insert_header', receiver: '^HttpResponse$' }, argIndex: 'all',
|
|
2133
|
+
vuln: { name: 'Header Injection / Open Redirect (response header set from input)', severity: 'medium', cwe: 'CWE-113',
|
|
2134
|
+
remediation: 'Validate the value against an allow-list and strip CR/LF; for Location headers only allow relative paths or allow-listed hosts.' } },
|
|
2135
|
+
// Log injection (low): request data written into structured logs unchanged.
|
|
2136
|
+
...['info', 'warn', 'error', 'debug', 'trace'].map(macro => (
|
|
2137
|
+
{ kind: 'sink', id: `rs-log-${macro}`, language: 'rs', framework: 'log/tracing', match: { type: 'call', callee: `${macro}!` }, argIndex: 0,
|
|
2138
|
+
vuln: { name: `Log Injection (${macro}! with input-derived content)`, severity: 'low', cwe: 'CWE-117',
|
|
2139
|
+
remediation: 'Log input as a structured field (`tracing::info!(user = %name, "login")`) or strip CR/LF so a request cannot forge log lines.' } }
|
|
2140
|
+
)),
|
|
2141
|
+
// Deserialization of input with format-level object construction.
|
|
2142
|
+
...['from_slice', 'from_reader', 'from_str'].map(method => (
|
|
2143
|
+
{ kind: 'sink', id: `rs-serde-pickle-${method}`, language: 'rs', framework: 'serde_pickle', match: { type: 'call', callee: method, receiver: '^serde_pickle$' }, argIndex: 0,
|
|
2144
|
+
vuln: { name: `Deserialization of Untrusted Data (serde_pickle::${method})`, severity: 'medium', cwe: 'CWE-502',
|
|
2145
|
+
remediation: 'Pickle can encode arbitrary object construction; deserialize input with serde_json/serde into a concrete typed struct instead.' } }
|
|
2146
|
+
)),
|
|
2147
|
+
{ kind: 'sink', id: 'rs-bincode-deserialize', language: 'rs', framework: 'bincode', match: { type: 'call', callee: 'deserialize', receiver: '^bincode$' }, argIndex: 0,
|
|
2148
|
+
vuln: { name: 'Deserialization of Untrusted Data (bincode::deserialize without size limits)', severity: 'medium', cwe: 'CWE-502',
|
|
2149
|
+
remediation: 'Use `bincode::DefaultOptions::new().with_limit(MAX)` so a length prefix from input cannot drive a huge allocation, and deserialize into a concrete type.' } },
|
|
2150
|
+
// Unbounded allocation driven by input.
|
|
2151
|
+
{ kind: 'sink', id: 'rs-with-capacity', language: 'rs', framework: 'std', match: { type: 'call', callee: 'with_capacity', receiver: '^(?:Vec|String|HashMap|HashSet|VecDeque|BTreeMap)$' }, argIndex: 0,
|
|
2152
|
+
vuln: { name: 'Uncontrolled Resource Consumption (with_capacity sized by input)', severity: 'medium', cwe: 'CWE-770',
|
|
2153
|
+
remediation: 'Clamp the requested size: `Vec::with_capacity(n.min(MAX_ITEMS))`, or grow lazily with `Vec::new()` and push.' } },
|
|
2154
|
+
|
|
2155
|
+
// ─── SANITIZERS (Rust) ────────────────────────────────────────────────────
|
|
2156
|
+
// `x.parse::<i64>()` is a numeric parse; `Url::parse` is not a sanitizer of
|
|
2157
|
+
// anything, so it is excluded by receiver.
|
|
2158
|
+
{ kind: 'sanitizer', id: 'rs-parse-numeric', language: 'rs', match: { type: 'call', callee: 'parse', receiverExclude: '^(?:Url|Uri|url|reqwest|Path|PathBuf|serde_json|json|toml|serde_yaml)$' }, effect: 'strip', appliesTo: ['*'] },
|
|
2159
|
+
{ kind: 'sanitizer', id: 'rs-html-escape-encode-text', language: 'rs', match: { type: 'call', callee: 'encode_text' }, effect: 'strip', appliesTo: ['xss'] },
|
|
2160
|
+
{ kind: 'sanitizer', id: 'rs-html-escape-encode-safe', language: 'rs', match: { type: 'call', callee: 'encode_safe' }, effect: 'strip', appliesTo: ['xss'] },
|
|
2161
|
+
{ kind: 'sanitizer', id: 'rs-html-escape-attr', language: 'rs', match: { type: 'call', callee: 'encode_double_quoted_attribute' }, effect: 'strip', appliesTo: ['xss'] },
|
|
2162
|
+
{ kind: 'sanitizer', id: 'rs-ammonia-clean', language: 'rs', match: { type: 'call', callee: 'clean', receiver: '^ammonia$' }, effect: 'strip', appliesTo: ['xss'] },
|
|
2163
|
+
{ kind: 'sanitizer', id: 'rs-askama-escape', language: 'rs', match: { type: 'call', callee: 'escape', receiver: '^(?:askama|v_htmlescape|html_escape|tera)$' }, effect: 'strip', appliesTo: ['xss'] },
|
|
2164
|
+
{ kind: 'sanitizer', id: 'rs-shell-escape', language: 'rs', match: { type: 'call', callee: 'escape', receiver: '^shell_escape$' }, effect: 'strip', appliesTo: ['cmd'] },
|
|
2165
|
+
{ kind: 'sanitizer', id: 'rs-shlex-quote', language: 'rs', match: { type: 'call', callee: 'quote', receiver: '^shlex$' }, effect: 'strip', appliesTo: ['cmd'] },
|
|
2166
|
+
{ kind: 'sanitizer', id: 'rs-shlex-try-quote', language: 'rs', match: { type: 'call', callee: 'try_quote', receiver: '^shlex$' }, effect: 'strip', appliesTo: ['cmd'] },
|
|
2167
|
+
{ kind: 'sanitizer', id: 'rs-sanitize-filename', language: 'rs', match: { type: 'call', callee: 'sanitize', receiver: '^sanitize_filename$' }, effect: 'strip', appliesTo: ['path'] },
|
|
2168
|
+
{ kind: 'sanitizer', id: 'rs-path-file-name', language: 'rs', match: { type: 'call', callee: 'file_name' }, effect: 'strip', appliesTo: ['path'] },
|
|
2169
|
+
{ kind: 'sanitizer', id: 'rs-percent-encode', language: 'rs', match: { type: 'call', callee: 'utf8_percent_encode' }, effect: 'strip', appliesTo: ['url'] },
|
|
1429
2170
|
];
|
|
1430
2171
|
|
|
1431
2172
|
// ─── Expanded sanitizer catalog (v0.65.0) ────────────────────────────────
|
|
@@ -1458,6 +2199,7 @@ const _LANG_EXT = {
|
|
|
1458
2199
|
php: /\.(?:php|phtml)$/i,
|
|
1459
2200
|
rb: /\.rb$/i,
|
|
1460
2201
|
java: /\.java$/i,
|
|
2202
|
+
rs: /\.rs$/i,
|
|
1461
2203
|
};
|
|
1462
2204
|
|
|
1463
2205
|
// cpp delegates to cppExtRe() rather than duplicating a literal set, so it
|
|
@@ -1778,7 +2520,25 @@ function _calleeIndexHits(calleeExpr) {
|
|
|
1778
2520
|
return raw;
|
|
1779
2521
|
}
|
|
1780
2522
|
|
|
1781
|
-
|
|
2523
|
+
// SARD_80_F1 W4.C39 — `receiverType` (optional 3rd param) is the SOURCE-side
|
|
2524
|
+
// counterpart of `matchSinkOrSanitizer`'s existing receiverType param, closing
|
|
2525
|
+
// a gap W4.C32 found and explicitly deferred: matchSource's CALL-shaped
|
|
2526
|
+
// branch used to filter ONLY via `_receiverAllowed` (a name regex), with no
|
|
2527
|
+
// way for a `receiverTypeIn`-bearing source entry (`cs-datareader-*`) to
|
|
2528
|
+
// confirm a match via CHA-resolved declared type the way a sink already
|
|
2529
|
+
// could. W4.C32 built and verified this exact mechanism once already, then
|
|
2530
|
+
// reverted it — at the time, `classOfVar`'s CHA only resolved `new Foo()`
|
|
2531
|
+
// allocations, and `SqlDataReader` has no public constructor (always
|
|
2532
|
+
// obtained via `command.ExecuteReader()`, a factory method), so the
|
|
2533
|
+
// mechanism was correct but had no way to ever actually resolve the type it
|
|
2534
|
+
// needed. That blocker is gone: `class-hierarchy.js`'s `typeOfVar` now also
|
|
2535
|
+
// captures a local's DECLARED type off `Type x = expr;` (parser-cs.js's
|
|
2536
|
+
// `declaredType`, added independently for a different consumer), which
|
|
2537
|
+
// covers a factory-returned value exactly as well as a `new` one — verified
|
|
2538
|
+
// directly that `classOfVar` now resolves `dr` to `SqlDataReader` for
|
|
2539
|
+
// `using (SqlDataReader dr = command.ExecuteReader())`. Re-attempting the
|
|
2540
|
+
// SAME mechanism W4.C32 already designed is what actually unlocks it now.
|
|
2541
|
+
export function matchSource(expr, file, receiverType) {
|
|
1782
2542
|
if (!expr) return null;
|
|
1783
2543
|
// Member sources (req.query): the original path — unchanged.
|
|
1784
2544
|
if (expr.kind === 'member' && expr.object?.kind === 'ident') {
|
|
@@ -1816,10 +2576,39 @@ export function matchSource(expr, file) {
|
|
|
1816
2576
|
if (raw.length) {
|
|
1817
2577
|
const hits = filterByProvenance(raw)
|
|
1818
2578
|
.filter(h => _languageAllowed(h, file))
|
|
1819
|
-
.filter(h => _receiverAllowed(h, expr.callee));
|
|
2579
|
+
.filter(h => _receiverAllowed(h, expr.callee) || _receiverTypeConfirms(h, receiverType));
|
|
1820
2580
|
const s = hits.find(h => h.kind === 'source');
|
|
1821
2581
|
if (s) return s;
|
|
1822
2582
|
}
|
|
2583
|
+
// SARD_80_F1 W4.C27 — a value-accessor method called ON an already-
|
|
2584
|
+
// registered MEMBER source (`req.Params.Get("name")`,
|
|
2585
|
+
// `req.QueryString.Get("id")` — .NET's `NameValueCollection` read API,
|
|
2586
|
+
// equivalent to its own indexer `req.Params["name"]`, which already
|
|
2587
|
+
// matched via the branch above) was invisible here: the callee is a
|
|
2588
|
+
// flat dotted STRING for C#'s hand-rolled parser ("req.Params.Get"),
|
|
2589
|
+
// and `_calleeIndexHits` only ever consults CALLEE_INDEX (call-type
|
|
2590
|
+
// catalog entries) — a MEMBER-type source like `cs-req-params` lives in
|
|
2591
|
+
// MEMBER_INDEX under the key "req.Params" and was never looked up here
|
|
2592
|
+
// at all. Confirmed against the public C# mirror
|
|
2593
|
+
// (CWE78_OS_Command_Injection__Params_Get_Web_01.cs): the indexer form
|
|
2594
|
+
// already fired via IR-TAINT; the `.Get(...)` form produced zero
|
|
2595
|
+
// findings. Strip the callee's last dotted segment and, when it's one
|
|
2596
|
+
// of the small set of known NameValueCollection read accessors, look
|
|
2597
|
+
// that prefix up in MEMBER_INDEX exactly as the plain-member branch
|
|
2598
|
+
// above does.
|
|
2599
|
+
if (typeof expr.callee === 'string' && expr.callee.includes('.')) {
|
|
2600
|
+
const lastDot = expr.callee.lastIndexOf('.');
|
|
2601
|
+
const method = expr.callee.slice(lastDot + 1);
|
|
2602
|
+
if (/^(?:Get|GetValues|GetKey)$/.test(method)) {
|
|
2603
|
+
const prefix = expr.callee.slice(0, lastDot);
|
|
2604
|
+
const raw2 = MEMBER_INDEX.get(prefix);
|
|
2605
|
+
if (raw2) {
|
|
2606
|
+
const hits = filterByProvenance(raw2).filter(h => _languageAllowed(h, file));
|
|
2607
|
+
const s = hits.find(h => h.kind === 'source');
|
|
2608
|
+
if (s) return s;
|
|
2609
|
+
}
|
|
2610
|
+
}
|
|
2611
|
+
}
|
|
1823
2612
|
}
|
|
1824
2613
|
return null;
|
|
1825
2614
|
}
|