circle-ir 4.17.0 → 4.18.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/analysis/advisories.d.ts +186 -0
- package/dist/analysis/advisories.d.ts.map +1 -0
- package/dist/analysis/advisories.js +391 -0
- package/dist/analysis/advisories.js.map +1 -0
- package/dist/analysis/comment-lines.d.ts +14 -0
- package/dist/analysis/comment-lines.d.ts.map +1 -0
- package/dist/analysis/comment-lines.js +34 -0
- package/dist/analysis/comment-lines.js.map +1 -0
- package/dist/analysis/config-loader.d.ts.map +1 -1
- package/dist/analysis/config-loader.js +59 -5
- package/dist/analysis/config-loader.js.map +1 -1
- package/dist/analysis/constant-caller-gate.d.ts +2 -2
- package/dist/analysis/constant-caller-gate.d.ts.map +1 -1
- package/dist/analysis/constant-caller-gate.js +205 -29
- package/dist/analysis/constant-caller-gate.js.map +1 -1
- package/dist/analysis/constant-propagation/propagator.d.ts +4 -0
- package/dist/analysis/constant-propagation/propagator.d.ts.map +1 -1
- package/dist/analysis/constant-propagation/propagator.js +99 -0
- package/dist/analysis/constant-propagation/propagator.js.map +1 -1
- package/dist/analysis/entry-point-detection.d.ts.map +1 -1
- package/dist/analysis/entry-point-detection.js +72 -0
- package/dist/analysis/entry-point-detection.js.map +1 -1
- package/dist/analysis/interprocedural.d.ts.map +1 -1
- package/dist/analysis/interprocedural.js +75 -8
- package/dist/analysis/interprocedural.js.map +1 -1
- package/dist/analysis/maven-declared.d.ts +72 -0
- package/dist/analysis/maven-declared.d.ts.map +1 -0
- package/dist/analysis/maven-declared.js +216 -0
- package/dist/analysis/maven-declared.js.map +1 -0
- package/dist/analysis/maven-version.d.ts +28 -0
- package/dist/analysis/maven-version.d.ts.map +1 -0
- package/dist/analysis/maven-version.js +172 -0
- package/dist/analysis/maven-version.js.map +1 -0
- package/dist/analysis/passes/constant-propagation-pass.d.ts.map +1 -1
- package/dist/analysis/passes/constant-propagation-pass.js +29 -1
- package/dist/analysis/passes/constant-propagation-pass.js.map +1 -1
- package/dist/analysis/passes/cross-file-pass.d.ts.map +1 -1
- package/dist/analysis/passes/cross-file-pass.js +81 -1
- package/dist/analysis/passes/cross-file-pass.js.map +1 -1
- package/dist/analysis/passes/csharp-sanitizer-credit.d.ts +7 -0
- package/dist/analysis/passes/csharp-sanitizer-credit.d.ts.map +1 -1
- package/dist/analysis/passes/csharp-sanitizer-credit.js +44 -1
- package/dist/analysis/passes/csharp-sanitizer-credit.js.map +1 -1
- package/dist/analysis/passes/insecure-cookie-pass.d.ts +2 -0
- package/dist/analysis/passes/insecure-cookie-pass.d.ts.map +1 -1
- package/dist/analysis/passes/insecure-cookie-pass.js +25 -3
- package/dist/analysis/passes/insecure-cookie-pass.js.map +1 -1
- package/dist/analysis/passes/interprocedural-pass.d.ts +11 -14
- package/dist/analysis/passes/interprocedural-pass.d.ts.map +1 -1
- package/dist/analysis/passes/interprocedural-pass.js +562 -0
- package/dist/analysis/passes/interprocedural-pass.js.map +1 -1
- package/dist/analysis/passes/language-sources-pass.d.ts +18 -1
- package/dist/analysis/passes/language-sources-pass.d.ts.map +1 -1
- package/dist/analysis/passes/language-sources-pass.js +1057 -14
- package/dist/analysis/passes/language-sources-pass.js.map +1 -1
- package/dist/analysis/passes/param-source-flow-gate-pass.d.ts.map +1 -1
- package/dist/analysis/passes/param-source-flow-gate-pass.js +66 -3
- package/dist/analysis/passes/param-source-flow-gate-pass.js.map +1 -1
- package/dist/analysis/passes/sink-filter-pass.d.ts +44 -2
- package/dist/analysis/passes/sink-filter-pass.d.ts.map +1 -1
- package/dist/analysis/passes/sink-filter-pass.js +690 -12
- package/dist/analysis/passes/sink-filter-pass.js.map +1 -1
- package/dist/analysis/passes/taint-propagation-pass.d.ts.map +1 -1
- package/dist/analysis/passes/taint-propagation-pass.js +133 -10
- package/dist/analysis/passes/taint-propagation-pass.js.map +1 -1
- package/dist/analysis/passes/tls-verify-disabled-pass.d.ts +18 -0
- package/dist/analysis/passes/tls-verify-disabled-pass.d.ts.map +1 -1
- package/dist/analysis/passes/tls-verify-disabled-pass.js +161 -0
- package/dist/analysis/passes/tls-verify-disabled-pass.js.map +1 -1
- package/dist/analysis/passes/weak-random-pass.d.ts +6 -0
- package/dist/analysis/passes/weak-random-pass.d.ts.map +1 -1
- package/dist/analysis/passes/weak-random-pass.js +29 -0
- package/dist/analysis/passes/weak-random-pass.js.map +1 -1
- package/dist/analysis/promote-cross-file-flows.d.ts +17 -0
- package/dist/analysis/promote-cross-file-flows.d.ts.map +1 -0
- package/dist/analysis/promote-cross-file-flows.js +32 -0
- package/dist/analysis/promote-cross-file-flows.js.map +1 -0
- package/dist/analysis/sbom.d.ts +100 -0
- package/dist/analysis/sbom.d.ts.map +1 -1
- package/dist/analysis/sbom.js +514 -3
- package/dist/analysis/sbom.js.map +1 -1
- package/dist/analysis/semver-version.d.ts +30 -0
- package/dist/analysis/semver-version.d.ts.map +1 -0
- package/dist/analysis/semver-version.js +76 -0
- package/dist/analysis/semver-version.js.map +1 -0
- package/dist/analysis/taint-matcher.d.ts +5 -0
- package/dist/analysis/taint-matcher.d.ts.map +1 -1
- package/dist/analysis/taint-matcher.js +225 -4
- package/dist/analysis/taint-matcher.js.map +1 -1
- package/dist/analyzer.d.ts +57 -0
- package/dist/analyzer.d.ts.map +1 -1
- package/dist/analyzer.js +69 -6
- package/dist/analyzer.js.map +1 -1
- package/dist/browser/circle-ir.js +2936 -291
- package/dist/core/circle-ir-core.cjs +634 -203
- package/dist/core/circle-ir-core.js +634 -203
- package/dist/core/extractors/dfg.js +205 -2
- package/dist/core/extractors/dfg.js.map +1 -1
- package/dist/core/extractors/ts-declaration-detail.d.ts +39 -0
- package/dist/core/extractors/ts-declaration-detail.d.ts.map +1 -0
- package/dist/core/extractors/ts-declaration-detail.js +266 -0
- package/dist/core/extractors/ts-declaration-detail.js.map +1 -0
- package/dist/graph/analysis-pass.d.ts +4 -1
- package/dist/graph/analysis-pass.d.ts.map +1 -1
- package/dist/graph/analysis-pass.js +4 -1
- package/dist/graph/analysis-pass.js.map +1 -1
- package/dist/graph-facts/dead-code.d.ts +62 -0
- package/dist/graph-facts/dead-code.d.ts.map +1 -0
- package/dist/graph-facts/dead-code.js +90 -0
- package/dist/graph-facts/dead-code.js.map +1 -0
- package/dist/graph-facts/dependency-calls.d.ts +86 -0
- package/dist/graph-facts/dependency-calls.d.ts.map +1 -0
- package/dist/graph-facts/dependency-calls.js +93 -0
- package/dist/graph-facts/dependency-calls.js.map +1 -0
- package/dist/graph-facts/index.d.ts +10 -0
- package/dist/graph-facts/index.d.ts.map +1 -0
- package/dist/graph-facts/index.js +10 -0
- package/dist/graph-facts/index.js.map +1 -0
- package/dist/graph-facts/maven-packages.d.ts +42 -0
- package/dist/graph-facts/maven-packages.d.ts.map +1 -0
- package/dist/graph-facts/maven-packages.js +133 -0
- package/dist/graph-facts/maven-packages.js.map +1 -0
- package/dist/graph-facts/reach.d.ts +203 -0
- package/dist/graph-facts/reach.d.ts.map +1 -0
- package/dist/graph-facts/reach.js +219 -0
- package/dist/graph-facts/reach.js.map +1 -0
- package/dist/graph-facts/removal-reach.d.ts +73 -0
- package/dist/graph-facts/removal-reach.d.ts.map +1 -0
- package/dist/graph-facts/removal-reach.js +64 -0
- package/dist/graph-facts/removal-reach.js.map +1 -0
- package/dist/index.d.ts +21 -6
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +20 -3
- package/dist/index.js.map +1 -1
- package/dist/navigation/go-graph.d.ts +68 -0
- package/dist/navigation/go-graph.d.ts.map +1 -0
- package/dist/navigation/go-graph.js +753 -0
- package/dist/navigation/go-graph.js.map +1 -0
- package/dist/navigation/index.d.ts +7 -1
- package/dist/navigation/index.d.ts.map +1 -1
- package/dist/navigation/index.js +6 -0
- package/dist/navigation/index.js.map +1 -1
- package/dist/navigation/java-entry-tables.d.ts +43 -0
- package/dist/navigation/java-entry-tables.d.ts.map +1 -0
- package/dist/navigation/java-entry-tables.js +56 -0
- package/dist/navigation/java-entry-tables.js.map +1 -0
- package/dist/navigation/java-graph.d.ts +64 -0
- package/dist/navigation/java-graph.d.ts.map +1 -0
- package/dist/navigation/java-graph.js +466 -0
- package/dist/navigation/java-graph.js.map +1 -0
- package/dist/navigation/navigation-index.d.ts +51 -1
- package/dist/navigation/navigation-index.d.ts.map +1 -1
- package/dist/navigation/navigation-index.js +72 -0
- package/dist/navigation/navigation-index.js.map +1 -1
- package/dist/navigation/python-graph.d.ts +73 -0
- package/dist/navigation/python-graph.d.ts.map +1 -0
- package/dist/navigation/python-graph.js +867 -0
- package/dist/navigation/python-graph.js.map +1 -0
- package/dist/navigation/ts-call-edges.d.ts +193 -0
- package/dist/navigation/ts-call-edges.d.ts.map +1 -0
- package/dist/navigation/ts-call-edges.js +2089 -0
- package/dist/navigation/ts-call-edges.js.map +1 -0
- package/dist/navigation/ts-entry-tables.d.ts +88 -0
- package/dist/navigation/ts-entry-tables.d.ts.map +1 -0
- package/dist/navigation/ts-entry-tables.js +50 -0
- package/dist/navigation/ts-entry-tables.js.map +1 -0
- package/dist/path-set/branches.d.ts +52 -0
- package/dist/path-set/branches.d.ts.map +1 -0
- package/dist/path-set/branches.js +124 -0
- package/dist/path-set/branches.js.map +1 -0
- package/dist/path-set/finding-diff.d.ts +63 -0
- package/dist/path-set/finding-diff.d.ts.map +1 -0
- package/dist/path-set/finding-diff.js +150 -0
- package/dist/path-set/finding-diff.js.map +1 -0
- package/dist/path-set/index.d.ts +18 -0
- package/dist/path-set/index.d.ts.map +1 -0
- package/dist/path-set/index.js +12 -0
- package/dist/path-set/index.js.map +1 -0
- package/dist/path-set/line-diff.d.ts +19 -0
- package/dist/path-set/line-diff.d.ts.map +1 -0
- package/dist/path-set/line-diff.js +98 -0
- package/dist/path-set/line-diff.js.map +1 -0
- package/dist/path-set/path-set.d.ts +88 -0
- package/dist/path-set/path-set.d.ts.map +1 -0
- package/dist/path-set/path-set.js +716 -0
- package/dist/path-set/path-set.js.map +1 -0
- package/dist/path-set/templates.d.ts +42 -0
- package/dist/path-set/templates.d.ts.map +1 -0
- package/dist/path-set/templates.js +119 -0
- package/dist/path-set/templates.js.map +1 -0
- package/dist/path-set/types.d.ts +195 -0
- package/dist/path-set/types.d.ts.map +1 -0
- package/dist/path-set/types.js +8 -0
- package/dist/path-set/types.js.map +1 -0
- package/dist/remedy-class/index.d.ts +12 -0
- package/dist/remedy-class/index.d.ts.map +1 -0
- package/dist/remedy-class/index.js +9 -0
- package/dist/remedy-class/index.js.map +1 -0
- package/dist/remedy-class/match.d.ts +165 -0
- package/dist/remedy-class/match.d.ts.map +1 -0
- package/dist/remedy-class/match.js +705 -0
- package/dist/remedy-class/match.js.map +1 -0
- package/dist/remedy-class/output-context.d.ts +25 -0
- package/dist/remedy-class/output-context.d.ts.map +1 -0
- package/dist/remedy-class/output-context.js +150 -0
- package/dist/remedy-class/output-context.js.map +1 -0
- package/dist/remedy-class/sha256.d.ts +18 -0
- package/dist/remedy-class/sha256.d.ts.map +1 -0
- package/dist/remedy-class/sha256.js +89 -0
- package/dist/remedy-class/sha256.js.map +1 -0
- package/dist/remedy-class/table.d.ts +130 -0
- package/dist/remedy-class/table.d.ts.map +1 -0
- package/dist/remedy-class/table.js +457 -0
- package/dist/remedy-class/table.js.map +1 -0
- package/dist/resolution/cross-file.d.ts +9 -0
- package/dist/resolution/cross-file.d.ts.map +1 -1
- package/dist/resolution/cross-file.js +60 -5
- package/dist/resolution/cross-file.js.map +1 -1
- package/dist/stable-ids/builder.d.ts +20 -0
- package/dist/stable-ids/builder.d.ts.map +1 -0
- package/dist/stable-ids/builder.js +65 -0
- package/dist/stable-ids/builder.js.map +1 -0
- package/dist/stable-ids/diff.d.ts +18 -0
- package/dist/stable-ids/diff.d.ts.map +1 -0
- package/dist/stable-ids/diff.js +37 -0
- package/dist/stable-ids/diff.js.map +1 -0
- package/dist/stable-ids/go.d.ts +28 -0
- package/dist/stable-ids/go.d.ts.map +1 -0
- package/dist/stable-ids/go.js +160 -0
- package/dist/stable-ids/go.js.map +1 -0
- package/dist/stable-ids/hash.d.ts +22 -0
- package/dist/stable-ids/hash.d.ts.map +1 -0
- package/dist/stable-ids/hash.js +167 -0
- package/dist/stable-ids/hash.js.map +1 -0
- package/dist/stable-ids/index.d.ts +30 -0
- package/dist/stable-ids/index.d.ts.map +1 -0
- package/dist/stable-ids/index.js +59 -0
- package/dist/stable-ids/index.js.map +1 -0
- package/dist/stable-ids/java.d.ts +28 -0
- package/dist/stable-ids/java.d.ts.map +1 -0
- package/dist/stable-ids/java.js +224 -0
- package/dist/stable-ids/java.js.map +1 -0
- package/dist/stable-ids/moves.d.ts +87 -0
- package/dist/stable-ids/moves.d.ts.map +1 -0
- package/dist/stable-ids/moves.js +222 -0
- package/dist/stable-ids/moves.js.map +1 -0
- package/dist/stable-ids/nodes.d.ts +32 -0
- package/dist/stable-ids/nodes.d.ts.map +1 -0
- package/dist/stable-ids/nodes.js +92 -0
- package/dist/stable-ids/nodes.js.map +1 -0
- package/dist/stable-ids/normalize.d.ts +35 -0
- package/dist/stable-ids/normalize.d.ts.map +1 -0
- package/dist/stable-ids/normalize.js +174 -0
- package/dist/stable-ids/normalize.js.map +1 -0
- package/dist/stable-ids/parse-errors.d.ts +24 -0
- package/dist/stable-ids/parse-errors.d.ts.map +1 -0
- package/dist/stable-ids/parse-errors.js +46 -0
- package/dist/stable-ids/parse-errors.js.map +1 -0
- package/dist/stable-ids/python.d.ts +27 -0
- package/dist/stable-ids/python.d.ts.map +1 -0
- package/dist/stable-ids/python.js +212 -0
- package/dist/stable-ids/python.js.map +1 -0
- package/dist/stable-ids/ts-parse-errors.d.ts +27 -0
- package/dist/stable-ids/ts-parse-errors.d.ts.map +1 -0
- package/dist/stable-ids/ts-parse-errors.js +75 -0
- package/dist/stable-ids/ts-parse-errors.js.map +1 -0
- package/dist/stable-ids/types.d.ts +106 -0
- package/dist/stable-ids/types.d.ts.map +1 -0
- package/dist/stable-ids/types.js +16 -0
- package/dist/stable-ids/types.js.map +1 -0
- package/dist/stable-ids/typescript.d.ts +30 -0
- package/dist/stable-ids/typescript.d.ts.map +1 -0
- package/dist/stable-ids/typescript.js +362 -0
- package/dist/stable-ids/typescript.js.map +1 -0
- package/dist/types/config.d.ts +9 -0
- package/dist/types/config.d.ts.map +1 -1
- package/dist/types/index.d.ts +35 -0
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/index.js.map +1 -1
- package/package.json +3 -3
|
@@ -20,6 +20,7 @@ import { javaCallArgument, javaCanonicalBaseLacksSeparator } from '../java-canon
|
|
|
20
20
|
import { findGoStringsReplacerSanitizers } from '../go-strings-replacer.js';
|
|
21
21
|
import { findRustLiteralEqualityGuardSanitizers } from '../rust-literal-guard.js';
|
|
22
22
|
import { constLookupTables, isConstTableLookup } from '../const-table-lookup.js';
|
|
23
|
+
import { jsReplaceChainVerdict } from '../js-crlf-strip.js';
|
|
23
24
|
// ---------------------------------------------------------------------------
|
|
24
25
|
// Pattern tables (moved from analyzer.ts)
|
|
25
26
|
// ---------------------------------------------------------------------------
|
|
@@ -298,6 +299,8 @@ export class LanguageSourcesPass {
|
|
|
298
299
|
}
|
|
299
300
|
additionalSanitizers.push(...findBashRegexAllowlistSanitizers(code));
|
|
300
301
|
additionalSanitizers.push(...findBashRealpathPrefixGuardSanitizers(code));
|
|
302
|
+
// #816 CVE-2026-8398: gpg/sha verify-then-install download scripts.
|
|
303
|
+
additionalSanitizers.push(...findBashVerifiedPackageDownloadSanitizers(code));
|
|
301
304
|
// #213 twelfth slice: printf '%q' + ${var@Q} shell-quote helpers.
|
|
302
305
|
additionalSanitizers.push(...findBashShellQuoteSanitizers(code));
|
|
303
306
|
}
|
|
@@ -417,8 +420,14 @@ export class LanguageSourcesPass {
|
|
|
417
420
|
}
|
|
418
421
|
// -- Rust: safe-handler sanitizer detectors (cognium-dev #115 Sprint 31) --
|
|
419
422
|
if (language === 'rust') {
|
|
423
|
+
additionalSanitizers.push(...findRustConstSliceMembershipGuardSanitizers(code));
|
|
420
424
|
additionalSanitizers.push(...findRustSetAllowlistGuardSanitizers(code));
|
|
421
425
|
additionalSanitizers.push(...findRustCanonicalizeGuardSanitizers(code));
|
|
426
|
+
// cognium-dev #816 — PAT-INPUT-01 rust safe mirror: parse + range guards
|
|
427
|
+
// before actix/axum response body (same credit model as JS twin).
|
|
428
|
+
additionalSanitizers.push(...findRustNumericRangeCheckGuardSanitizers(code));
|
|
429
|
+
// cognium-dev #816 — PAT-INFO-01: response echoes only query params, no env secrets.
|
|
430
|
+
additionalSanitizers.push(...findRustPatInfoHttpOnlyResponseSanitizers(code));
|
|
422
431
|
// cognium-dev#293: `if x != "a" && x != "b" { return Err(..) }` literal-equality guard.
|
|
423
432
|
additionalSanitizers.push(...findRustLiteralEqualityGuardSanitizers(code));
|
|
424
433
|
// Sprint 77a (#216 Pattern X): argv-form Command::new(literal).arg(...)
|
|
@@ -484,6 +493,9 @@ export class LanguageSourcesPass {
|
|
|
484
493
|
// #223 REG-144-17/18/19: JS/TS command allowlist guard
|
|
485
494
|
// (Set.has / Array.includes / indexOf) before execFile/spawn.
|
|
486
495
|
additionalSanitizers.push(...findJsCommandAllowlistGuardSanitizers(code));
|
|
496
|
+
// cognium-dev #816 — PAT-INPUT-01 js safe mirror: Number/parseInt
|
|
497
|
+
// coercion + finite/range guards before template response.
|
|
498
|
+
additionalSanitizers.push(...findJsNumericRangeCheckGuardSanitizers(code));
|
|
487
499
|
// Sprint 78 (#190): JS misconfig pattern findings — libxmljs noent:true.
|
|
488
500
|
for (const finding of findJsPatternFindings(code, graph.ir.meta.file)) {
|
|
489
501
|
ctx.addFinding(finding);
|
|
@@ -517,6 +529,9 @@ export class LanguageSourcesPass {
|
|
|
517
529
|
for (const finding of findJsLdapInjectionFindings(code, graph.ir.meta.file)) {
|
|
518
530
|
ctx.addFinding(finding);
|
|
519
531
|
}
|
|
532
|
+
for (const finding of findJsZipHeaderFailopenFindings(code, graph.ir.meta.file)) {
|
|
533
|
+
ctx.addFinding(finding);
|
|
534
|
+
}
|
|
520
535
|
// Sprint 89 (#189): JS insecure_deserialization —
|
|
521
536
|
// JSON.parse(req.body) on raw / text bodies.
|
|
522
537
|
for (const finding of findJsJsonParseBodyFindings(code, graph.ir.meta.file)) {
|
|
@@ -576,6 +591,9 @@ export class LanguageSourcesPass {
|
|
|
576
591
|
additionalSanitizers.push(...findJavaInlineCrlfStripLogSanitizers(code));
|
|
577
592
|
// cognium-dev#293: the same strip, but living in a user-defined helper.
|
|
578
593
|
additionalSanitizers.push(...findCrlfWrapperFunctionSanitizers(code, 'java'));
|
|
594
|
+
// cognium-dev#947: flow-sanitize ssti custom_wrapper — brace-stripping
|
|
595
|
+
// helper at Velocity.evaluate call site.
|
|
596
|
+
additionalSanitizers.push(...findJavaTemplateDelimiterWrapperSanitizers(code));
|
|
579
597
|
// Sprint 77a (#216 Pattern X): argv-form exec sanitizer.
|
|
580
598
|
additionalSanitizers.push(...findJavaArgvFormExecSanitizers(code));
|
|
581
599
|
// Sprint 78 (#190): Java misconfig pattern findings —
|
|
@@ -2851,6 +2869,171 @@ function computeDeadPythonBranchLines(lines) {
|
|
|
2851
2869
|
}
|
|
2852
2870
|
return dead;
|
|
2853
2871
|
}
|
|
2872
|
+
/**
|
|
2873
|
+
* Line indices (0-based) in unreachable `if`/`else` arms when the condition is
|
|
2874
|
+
* decidable from tracked integer locals (`int num = 86; if ((7*42)-num > 200) …`).
|
|
2875
|
+
*
|
|
2876
|
+
* Targets OWASP Benchmark shapes where the else arm assigns a parameter but
|
|
2877
|
+
* can never run (cognium-dev#913). Braced bodies defer to constant propagation;
|
|
2878
|
+
* this pass handles the common single-statement `if` / `else` lines.
|
|
2879
|
+
*/
|
|
2880
|
+
function parseJavaSwitchCaseLabel(label) {
|
|
2881
|
+
const t = label.trim();
|
|
2882
|
+
if (t === 'default')
|
|
2883
|
+
return 'default';
|
|
2884
|
+
const ch = t.match(/^'(\\.|[^'])'\s*$/);
|
|
2885
|
+
if (ch) {
|
|
2886
|
+
const inner = ch[1];
|
|
2887
|
+
if (inner.startsWith('\\')) {
|
|
2888
|
+
if (inner === "\\'")
|
|
2889
|
+
return 39;
|
|
2890
|
+
if (inner === '\\\\')
|
|
2891
|
+
return 92;
|
|
2892
|
+
if (inner === '\\n')
|
|
2893
|
+
return 10;
|
|
2894
|
+
return undefined;
|
|
2895
|
+
}
|
|
2896
|
+
return inner.charCodeAt(0);
|
|
2897
|
+
}
|
|
2898
|
+
const num = t.match(/^(-?\d+)\s*$/);
|
|
2899
|
+
if (num)
|
|
2900
|
+
return Number(num[1]);
|
|
2901
|
+
return undefined;
|
|
2902
|
+
}
|
|
2903
|
+
function parseJavaSwitchLabelLine(line) {
|
|
2904
|
+
const caseLine = line.match(/^\s*case\s+(.+?)\s*:\s*$/);
|
|
2905
|
+
if (caseLine)
|
|
2906
|
+
return parseJavaSwitchCaseLabel(caseLine[1]);
|
|
2907
|
+
if (/^\s*default\s*:\s*$/.test(line))
|
|
2908
|
+
return 'default';
|
|
2909
|
+
return undefined;
|
|
2910
|
+
}
|
|
2911
|
+
function markDeadJavaSwitchBodies(lines, switchLineIdx, switchValue, dead) {
|
|
2912
|
+
let depth = 0;
|
|
2913
|
+
let started = false;
|
|
2914
|
+
let groupLabels = [];
|
|
2915
|
+
let bodyStart = -1;
|
|
2916
|
+
let explicitCaseMatched = false;
|
|
2917
|
+
const groupIsTaken = () => {
|
|
2918
|
+
if (groupLabels.length === 0)
|
|
2919
|
+
return false;
|
|
2920
|
+
const explicit = groupLabels.some(v => v !== 'default' && v === switchValue);
|
|
2921
|
+
if (explicit)
|
|
2922
|
+
return true;
|
|
2923
|
+
return groupLabels.includes('default') && !explicitCaseMatched;
|
|
2924
|
+
};
|
|
2925
|
+
const flushGroup = (bodyEnd) => {
|
|
2926
|
+
if (bodyStart < 0 || groupLabels.length === 0)
|
|
2927
|
+
return;
|
|
2928
|
+
const taken = groupIsTaken();
|
|
2929
|
+
if (groupLabels.some(v => v !== 'default' && v === switchValue)) {
|
|
2930
|
+
explicitCaseMatched = true;
|
|
2931
|
+
}
|
|
2932
|
+
if (!taken) {
|
|
2933
|
+
for (let k = bodyStart; k < bodyEnd; k++)
|
|
2934
|
+
dead.add(k);
|
|
2935
|
+
}
|
|
2936
|
+
groupLabels = [];
|
|
2937
|
+
bodyStart = -1;
|
|
2938
|
+
};
|
|
2939
|
+
for (let j = switchLineIdx; j < lines.length; j++) {
|
|
2940
|
+
const line = lines[j];
|
|
2941
|
+
for (const ch of line) {
|
|
2942
|
+
if (ch === '{') {
|
|
2943
|
+
depth++;
|
|
2944
|
+
started = true;
|
|
2945
|
+
}
|
|
2946
|
+
else if (ch === '}')
|
|
2947
|
+
depth--;
|
|
2948
|
+
}
|
|
2949
|
+
if (!started)
|
|
2950
|
+
continue;
|
|
2951
|
+
const label = parseJavaSwitchLabelLine(line);
|
|
2952
|
+
if (label !== undefined) {
|
|
2953
|
+
if (bodyStart >= 0 && j === bodyStart) {
|
|
2954
|
+
groupLabels.push(label);
|
|
2955
|
+
bodyStart = j + 1;
|
|
2956
|
+
}
|
|
2957
|
+
else {
|
|
2958
|
+
flushGroup(j);
|
|
2959
|
+
groupLabels = [label];
|
|
2960
|
+
bodyStart = j + 1;
|
|
2961
|
+
}
|
|
2962
|
+
}
|
|
2963
|
+
if (started && depth === 0) {
|
|
2964
|
+
flushGroup(j);
|
|
2965
|
+
break;
|
|
2966
|
+
}
|
|
2967
|
+
}
|
|
2968
|
+
}
|
|
2969
|
+
export function computeDeadJavaBranchLines(lines) {
|
|
2970
|
+
const dead = new Set();
|
|
2971
|
+
const consts = new Map();
|
|
2972
|
+
const stringConsts = new Map();
|
|
2973
|
+
const charConsts = new Map();
|
|
2974
|
+
for (let i = 0; i < lines.length; i++) {
|
|
2975
|
+
const line = lines[i];
|
|
2976
|
+
const stringAssign = line.match(/^\s*String\s+([A-Za-z_]\w*)\s*=\s*"([^"]*)"\s*;\s*$/);
|
|
2977
|
+
if (stringAssign) {
|
|
2978
|
+
stringConsts.set(stringAssign[1], stringAssign[2]);
|
|
2979
|
+
continue;
|
|
2980
|
+
}
|
|
2981
|
+
const charAtAssign = line.match(/^\s*char\s+([A-Za-z_]\w*)\s*=\s*([A-Za-z_]\w*)\.charAt\s*\(\s*(\d+)\s*\)\s*;\s*(?:\/\/.*)?$/);
|
|
2982
|
+
if (charAtAssign) {
|
|
2983
|
+
const str = stringConsts.get(charAtAssign[2]);
|
|
2984
|
+
const idx = Number(charAtAssign[3]);
|
|
2985
|
+
if (str !== undefined && idx >= 0 && idx < str.length) {
|
|
2986
|
+
charConsts.set(charAtAssign[1], str.charCodeAt(idx));
|
|
2987
|
+
}
|
|
2988
|
+
else {
|
|
2989
|
+
charConsts.delete(charAtAssign[1]);
|
|
2990
|
+
}
|
|
2991
|
+
continue;
|
|
2992
|
+
}
|
|
2993
|
+
const intAssign = line.match(/^\s*(?:int|long|short|byte)\s+([A-Za-z_]\w*)\s*=\s*(.+?);\s*$/);
|
|
2994
|
+
if (intAssign) {
|
|
2995
|
+
const v = evaluatePythonConstExpression(intAssign[2].trim(), consts);
|
|
2996
|
+
if (typeof v === 'number')
|
|
2997
|
+
consts.set(intAssign[1], v);
|
|
2998
|
+
else
|
|
2999
|
+
consts.delete(intAssign[1]);
|
|
3000
|
+
continue;
|
|
3001
|
+
}
|
|
3002
|
+
const switchMatch = line.match(/^\s*switch\s*\(([^)]+)\)\s*\{?\s*$/);
|
|
3003
|
+
if (switchMatch) {
|
|
3004
|
+
const sel = switchMatch[1].trim();
|
|
3005
|
+
let switchVal = charConsts.get(sel);
|
|
3006
|
+
if (switchVal === undefined) {
|
|
3007
|
+
const n = evaluatePythonConstExpression(sel, consts);
|
|
3008
|
+
if (typeof n === 'number')
|
|
3009
|
+
switchVal = n;
|
|
3010
|
+
}
|
|
3011
|
+
if (switchVal !== undefined) {
|
|
3012
|
+
markDeadJavaSwitchBodies(lines, i, switchVal, dead);
|
|
3013
|
+
}
|
|
3014
|
+
continue;
|
|
3015
|
+
}
|
|
3016
|
+
const ifMatch = line.match(/^(\s*)if\s*\((.+)\)\s+(.+)$/);
|
|
3017
|
+
if (!ifMatch)
|
|
3018
|
+
continue;
|
|
3019
|
+
const decided = evaluatePythonConstExpression(ifMatch[2].trim(), consts);
|
|
3020
|
+
if (decided !== true && decided !== false)
|
|
3021
|
+
continue;
|
|
3022
|
+
const rest = ifMatch[3].trim();
|
|
3023
|
+
if (/\belse\b/.test(rest))
|
|
3024
|
+
continue;
|
|
3025
|
+
if (i + 1 < lines.length && /^\s*else\b/.test(lines[i + 1])) {
|
|
3026
|
+
if (decided === true)
|
|
3027
|
+
dead.add(i + 1);
|
|
3028
|
+
else
|
|
3029
|
+
dead.add(i);
|
|
3030
|
+
}
|
|
3031
|
+
else if (decided === false) {
|
|
3032
|
+
dead.add(i);
|
|
3033
|
+
}
|
|
3034
|
+
}
|
|
3035
|
+
return dead;
|
|
3036
|
+
}
|
|
2854
3037
|
export function buildPythonTaintedVars(sourceCode) {
|
|
2855
3038
|
const tainted = new Map();
|
|
2856
3039
|
const containerTainted = new Map();
|
|
@@ -3916,6 +4099,113 @@ export function buildRustTaintedVars(sourceCode, seedVars) {
|
|
|
3916
4099
|
* on the way (a lambda, anonymous class, switch expression or block is not a
|
|
3917
4100
|
* plain wrapped value).
|
|
3918
4101
|
*/
|
|
4102
|
+
/** `code(); // note` → `code();` — a `//` inside a string or char literal is kept. */
|
|
4103
|
+
function stripJavaTrailingLineComment(line) {
|
|
4104
|
+
let quote = null;
|
|
4105
|
+
for (let i = 0; i < line.length; i++) {
|
|
4106
|
+
const c = line[i];
|
|
4107
|
+
if (quote) {
|
|
4108
|
+
if (c === '\\')
|
|
4109
|
+
i++;
|
|
4110
|
+
else if (c === quote)
|
|
4111
|
+
quote = null;
|
|
4112
|
+
}
|
|
4113
|
+
else if (c === '"' || c === "'") {
|
|
4114
|
+
quote = c;
|
|
4115
|
+
}
|
|
4116
|
+
else if (c === '/' && line[i + 1] === '/') {
|
|
4117
|
+
return line.slice(0, i).trimEnd();
|
|
4118
|
+
}
|
|
4119
|
+
}
|
|
4120
|
+
return line;
|
|
4121
|
+
}
|
|
4122
|
+
/** Identifiers at code positions in a Java expression (string and char literal bodies dropped). */
|
|
4123
|
+
function javaExpressionIdents(expr) {
|
|
4124
|
+
const code = expr.replace(/"(?:[^"\\]|\\.)*"/g, ' ').replace(/'(?:[^'\\]|\\.)'/g, ' ');
|
|
4125
|
+
return code.match(/[\p{L}\p{N}_]+/gu) ?? [];
|
|
4126
|
+
}
|
|
4127
|
+
/**
|
|
4128
|
+
* `buildJavaTaintedVars` slot tracking (cognium-dev#913/#914/#916/#917). Records
|
|
4129
|
+
* `list.add(x)` / `list.remove(<int>)` / `map.put("<key>", x)` into the slot
|
|
4130
|
+
* tables and returns true for those statements. An `int` constant declaration
|
|
4131
|
+
* is recorded for ternary folding but returns false, so the line is still read
|
|
4132
|
+
* as an assignment.
|
|
4133
|
+
*/
|
|
4134
|
+
function recordJavaSlotStatement(line, listElems, mapSlots, intConsts) {
|
|
4135
|
+
const t = line.trimEnd().replace(/;\s*\/\/[^"]*$/, ';');
|
|
4136
|
+
const intDecl = /^\s*(?:final\s+)?(?:int|long|short|byte)\s+([A-Za-z_]\w*)\s*=\s*([^;]+);\s*$/.exec(t);
|
|
4137
|
+
if (intDecl) {
|
|
4138
|
+
const v = evaluatePythonConstExpression(intDecl[2].trim(), intConsts);
|
|
4139
|
+
if (typeof v === 'number')
|
|
4140
|
+
intConsts.set(intDecl[1], v);
|
|
4141
|
+
else
|
|
4142
|
+
intConsts.delete(intDecl[1]);
|
|
4143
|
+
return false;
|
|
4144
|
+
}
|
|
4145
|
+
const add = /^\s*([A-Za-z_]\w*)\.add\s*\(\s*([A-Za-z_]\w*|"(?:[^"\\]|\\.)*")\s*\)\s*;\s*$/.exec(t);
|
|
4146
|
+
if (add) {
|
|
4147
|
+
const elems = listElems.get(add[1]) ?? [];
|
|
4148
|
+
elems.push(add[2].startsWith('"') ? null : add[2]);
|
|
4149
|
+
listElems.set(add[1], elems);
|
|
4150
|
+
return true;
|
|
4151
|
+
}
|
|
4152
|
+
const remove = /^\s*([A-Za-z_]\w*)\.remove\s*\(\s*(\d+)\s*\)\s*;\s*$/.exec(t);
|
|
4153
|
+
if (remove && listElems.has(remove[1])) {
|
|
4154
|
+
const elems = listElems.get(remove[1]);
|
|
4155
|
+
const idx = Number(remove[2]);
|
|
4156
|
+
if (idx < elems.length)
|
|
4157
|
+
elems.splice(idx, 1);
|
|
4158
|
+
else
|
|
4159
|
+
listElems.delete(remove[1]); // an index we did not see added: stop trusting the slots
|
|
4160
|
+
return true;
|
|
4161
|
+
}
|
|
4162
|
+
const put = /^\s*([A-Za-z_]\w*)\.put\s*\(\s*"((?:[^"\\]|\\.)*)"\s*,\s*(.+?)\s*\)\s*;\s*$/.exec(t);
|
|
4163
|
+
if (put) {
|
|
4164
|
+
const keys = mapSlots.get(put[1]) ?? new Map();
|
|
4165
|
+
keys.set(put[2], javaExpressionIdents(put[3]));
|
|
4166
|
+
mapSlots.set(put[1], keys);
|
|
4167
|
+
return true;
|
|
4168
|
+
}
|
|
4169
|
+
// Any other use of a tracked collection that is not a plain read (passed to a
|
|
4170
|
+
// call, `addAll`, `set`, `clear`, a non-literal `put`, re-declared …) may change
|
|
4171
|
+
// its contents in a way the tables do not model: stop trusting its slots.
|
|
4172
|
+
for (const table of [listElems, mapSlots]) {
|
|
4173
|
+
for (const name of [...table.keys()]) {
|
|
4174
|
+
const mention = new RegExp(`(?<![\\w.])${name}(?!\\w)`);
|
|
4175
|
+
if (!mention.test(t))
|
|
4176
|
+
continue;
|
|
4177
|
+
const reads = new RegExp(`(?<![\\w.])${name}\\.(?:get|size|isEmpty|contains\\w*)\\s*\\(`, 'g');
|
|
4178
|
+
if (mention.test(t.replace(reads, ' ')))
|
|
4179
|
+
table.delete(name);
|
|
4180
|
+
}
|
|
4181
|
+
}
|
|
4182
|
+
return false;
|
|
4183
|
+
}
|
|
4184
|
+
/**
|
|
4185
|
+
* The identifiers a `list.get(<int>)` / `map.get("<key>")` read (optionally
|
|
4186
|
+
* cast) can carry, from the slot tables; `undefined` when the RHS is not such a
|
|
4187
|
+
* read or the slots are unknown, so the caller falls back to whole-RHS matching.
|
|
4188
|
+
*/
|
|
4189
|
+
function javaSlotRead(rhs, listElems, mapSlots) {
|
|
4190
|
+
const m = /^(?:\(\s*[A-Za-z_][\w.<>]*\s*\)\s*)?([A-Za-z_]\w*)\.get\s*\(\s*(\d+|"(?:[^"\\]|\\.)*")\s*\)$/.exec(rhs.trim());
|
|
4191
|
+
if (!m)
|
|
4192
|
+
return undefined;
|
|
4193
|
+
const [, recv, arg] = m;
|
|
4194
|
+
if (arg.startsWith('"')) {
|
|
4195
|
+
const keys = mapSlots.get(recv);
|
|
4196
|
+
if (!keys)
|
|
4197
|
+
return undefined;
|
|
4198
|
+
return keys.get(arg.slice(1, -1)) ?? [];
|
|
4199
|
+
}
|
|
4200
|
+
const elems = listElems.get(recv);
|
|
4201
|
+
if (!elems)
|
|
4202
|
+
return undefined;
|
|
4203
|
+
const idx = Number(arg);
|
|
4204
|
+
if (idx >= elems.length)
|
|
4205
|
+
return undefined;
|
|
4206
|
+
const slot = elems[idx];
|
|
4207
|
+
return slot === null ? [] : [slot];
|
|
4208
|
+
}
|
|
3919
4209
|
function joinWrappedJavaStatement(lines, i) {
|
|
3920
4210
|
const strip = (l) => l.replace(/"(?:[^"\\]|\\.)*"|'(?:[^'\\]|\\.)'/g, '""').replace(/\/\/.*$/, '');
|
|
3921
4211
|
const first = strip(lines[i]);
|
|
@@ -3962,10 +4252,30 @@ function joinWrappedJavaStatement(lines, i) {
|
|
|
3962
4252
|
* consumed by `detectExpressionScanFlows` in `taint-propagation-pass.ts`
|
|
3963
4253
|
* to synthesize a virtual source for each derived var.
|
|
3964
4254
|
*/
|
|
3965
|
-
export function buildJavaTaintedVars(sourceCode, seedVars
|
|
4255
|
+
export function buildJavaTaintedVars(sourceCode, seedVars, deadBranchLines,
|
|
4256
|
+
/**
|
|
4257
|
+
* `trackSlots` (cognium-dev#913/#914/#916/#917): follow which element of a list
|
|
4258
|
+
* and which literal key of a map holds taint, and pick the branch of a ternary
|
|
4259
|
+
* whose condition folds to a constant int comparison. Only the helper-return
|
|
4260
|
+
* gate in InterproceduralPass asks for it; every other caller keeps the
|
|
4261
|
+
* whole-collection behaviour.
|
|
4262
|
+
*/
|
|
4263
|
+
options) {
|
|
3966
4264
|
const derived = new Map();
|
|
3967
4265
|
const knownTainted = new Set(seedVars);
|
|
3968
|
-
const
|
|
4266
|
+
const trackSlots = options?.trackSlots === true;
|
|
4267
|
+
// Slot mode also reads `x = list.get(0); // note` as the statement it is: the
|
|
4268
|
+
// assignment patterns below are anchored at `;`.
|
|
4269
|
+
const lines = trackSlots
|
|
4270
|
+
? sourceCode.split('\n').map(stripJavaTrailingLineComment)
|
|
4271
|
+
: sourceCode.split('\n');
|
|
4272
|
+
const dead = deadBranchLines ?? new Set();
|
|
4273
|
+
/** list var → element slots: the added identifier, or `null` for a literal. */
|
|
4274
|
+
const listElems = new Map();
|
|
4275
|
+
/** map var → literal key → the identifiers its stored value mentions. */
|
|
4276
|
+
const mapSlots = new Map();
|
|
4277
|
+
/** int locals with a constant value, for folding a ternary condition. */
|
|
4278
|
+
const intConsts = new Map();
|
|
3969
4279
|
// Declaration: [modifiers] Type[<...>] [ ] lhs = rhs;
|
|
3970
4280
|
// Simple assignment: lhs = rhs;
|
|
3971
4281
|
// The declaration form is tried first because a Type token in front
|
|
@@ -3980,6 +4290,8 @@ export function buildJavaTaintedVars(sourceCode, seedVars) {
|
|
|
3980
4290
|
// original var directly.
|
|
3981
4291
|
const declRe = /^\s*(?:public|private|protected|static|final|volatile|transient|\s)*\s*(?:[A-Za-z_][\w.]*(?:\s*<[^>]*>)?(?:\s*\[\s*\])*)\s+([A-Za-z_]\w*)\s*=\s*(.+?);\s*$/;
|
|
3982
4292
|
const assignRe = /^\s*([A-Za-z_]\w*)\s*=\s*(.+?);\s*$/;
|
|
4293
|
+
const thisFieldAssignRe = /^\s*this\.([A-Za-z_]\w*)\s*=\s*(.+?);\s*$/;
|
|
4294
|
+
const beanSetterCallRe = /\bset([A-Z]\w*)\s*\((.*)\)\s*;?\s*$/;
|
|
3983
4295
|
const JAVA_KEYWORDS = new Set([
|
|
3984
4296
|
'if', 'else', 'while', 'for', 'do', 'switch', 'case', 'return',
|
|
3985
4297
|
'throw', 'try', 'catch', 'finally', 'new', 'this', 'super',
|
|
@@ -4003,11 +4315,28 @@ export function buildJavaTaintedVars(sourceCode, seedVars) {
|
|
|
4003
4315
|
while (changed && guard < lines.length + 2) {
|
|
4004
4316
|
changed = false;
|
|
4005
4317
|
guard++;
|
|
4318
|
+
// Slot state is rebuilt on every pass: it describes the code top to bottom,
|
|
4319
|
+
// while `knownTainted` only grows.
|
|
4320
|
+
listElems.clear();
|
|
4321
|
+
mapSlots.clear();
|
|
4322
|
+
intConsts.clear();
|
|
4006
4323
|
for (let i = 0; i < lines.length; i++) {
|
|
4324
|
+
if (deadBranchLines !== undefined && dead.has(i))
|
|
4325
|
+
continue;
|
|
4007
4326
|
const line = lines[i];
|
|
4008
4327
|
const trimmed = line.trimStart();
|
|
4009
4328
|
if (trimmed.startsWith('//') || trimmed.startsWith('*'))
|
|
4010
4329
|
continue;
|
|
4330
|
+
if (trackSlots && recordJavaSlotStatement(line, listElems, mapSlots, intConsts)) {
|
|
4331
|
+
// `list.add(x)` still taints the list as a whole, as below.
|
|
4332
|
+
const add = /^\s*([A-Za-z_]\w*)\.add\s*\(\s*([A-Za-z_]\w*)\s*\)\s*;/.exec(line);
|
|
4333
|
+
if (add && knownTainted.has(add[2]) && !knownTainted.has(add[1])) {
|
|
4334
|
+
derived.set(add[1], i + 1);
|
|
4335
|
+
knownTainted.add(add[1]);
|
|
4336
|
+
changed = true;
|
|
4337
|
+
}
|
|
4338
|
+
continue;
|
|
4339
|
+
}
|
|
4011
4340
|
// cognium-dev#476: non-assignment statements (must run before the #753
|
|
4012
4341
|
// wrap-join early-continue, which would skip them).
|
|
4013
4342
|
const arrayCopyRe = /\bSystem\.arraycopy\s*\(\s*([A-Za-z_]\w*)\s*,\s*[^,]+\s*,\s*([A-Za-z_]\w*)\s*,/;
|
|
@@ -4042,21 +4371,92 @@ export function buildJavaTaintedVars(sourceCode, seedVars) {
|
|
|
4042
4371
|
// statement whose call argument list wraps over several lines reads like
|
|
4043
4372
|
// the same statement on one line.
|
|
4044
4373
|
const joined = joinWrappedJavaStatement(lines, i);
|
|
4045
|
-
if (joined
|
|
4046
|
-
|
|
4047
|
-
|
|
4048
|
-
|
|
4049
|
-
|
|
4374
|
+
if (joined !== null) {
|
|
4375
|
+
statement = joined;
|
|
4376
|
+
declMatch = declRe.exec(statement);
|
|
4377
|
+
assignMatch = !declMatch ? assignRe.exec(statement) : null;
|
|
4378
|
+
}
|
|
4379
|
+
}
|
|
4380
|
+
const thisFieldMatch = thisFieldAssignRe.exec(statement.trim());
|
|
4381
|
+
if (thisFieldMatch) {
|
|
4382
|
+
const fieldName = thisFieldMatch[1];
|
|
4383
|
+
const rhs = thisFieldMatch[2];
|
|
4384
|
+
const rhsCode = rhs
|
|
4385
|
+
.replace(/\$@?"(?:[^"\\]|\\.)*"/g, lit => ' ' + [...lit.matchAll(/\{([^}]*)\}/g)].map(x => x[1]).join(' ') + ' ')
|
|
4386
|
+
.replace(/@?"(?:[^"\\]|\\.)*"/g, ' ')
|
|
4387
|
+
.replace(/'(?:[^'\\]|\\.)'/g, ' ');
|
|
4388
|
+
const rhsIdents = rhsCode.match(/[\p{L}\p{N}_]+/gu);
|
|
4389
|
+
const ref = (rhsIdents !== null && rhsIdents.some(t => knownTainted.has(t))) ||
|
|
4390
|
+
(complexTaintedRes.length > 0 && complexTaintedRes.some(re => re.test(rhsCode)));
|
|
4391
|
+
if (ref) {
|
|
4392
|
+
const thisField = `this.${fieldName}`;
|
|
4393
|
+
if (!knownTainted.has(thisField)) {
|
|
4394
|
+
derived.set(thisField, i + 1);
|
|
4395
|
+
knownTainted.add(thisField);
|
|
4396
|
+
changed = true;
|
|
4397
|
+
}
|
|
4398
|
+
if (!knownTainted.has(fieldName)) {
|
|
4399
|
+
derived.set(fieldName, i + 1);
|
|
4400
|
+
knownTainted.add(fieldName);
|
|
4401
|
+
changed = true;
|
|
4402
|
+
}
|
|
4403
|
+
}
|
|
4404
|
+
continue;
|
|
4405
|
+
}
|
|
4406
|
+
const setterCall = beanSetterCallRe.exec(statement);
|
|
4407
|
+
if (setterCall) {
|
|
4408
|
+
const fieldName = setterCall[1].charAt(0).toLowerCase() + setterCall[1].slice(1);
|
|
4409
|
+
const arg = setterCall[2];
|
|
4410
|
+
const rhsCode = arg
|
|
4411
|
+
.replace(/\$@?"(?:[^"\\]|\\.)*"/g, lit => ' ' + [...lit.matchAll(/\{([^}]*)\}/g)].map(x => x[1]).join(' ') + ' ')
|
|
4412
|
+
.replace(/@?"(?:[^"\\]|\\.)*"/g, ' ')
|
|
4413
|
+
.replace(/'(?:[^'\\]|\\.)'/g, ' ');
|
|
4414
|
+
const rhsIdents = rhsCode.match(/[\p{L}\p{N}_]+/gu);
|
|
4415
|
+
const ref = (rhsIdents !== null && rhsIdents.some(t => knownTainted.has(t))) ||
|
|
4416
|
+
(complexTaintedRes.length > 0 && complexTaintedRes.some(re => re.test(rhsCode)));
|
|
4417
|
+
if (ref) {
|
|
4418
|
+
const thisField = `this.${fieldName}`;
|
|
4419
|
+
if (!knownTainted.has(thisField)) {
|
|
4420
|
+
derived.set(thisField, i + 1);
|
|
4421
|
+
knownTainted.add(thisField);
|
|
4422
|
+
changed = true;
|
|
4423
|
+
}
|
|
4424
|
+
if (!knownTainted.has(fieldName)) {
|
|
4425
|
+
derived.set(fieldName, i + 1);
|
|
4426
|
+
knownTainted.add(fieldName);
|
|
4427
|
+
changed = true;
|
|
4428
|
+
}
|
|
4429
|
+
}
|
|
4430
|
+
continue;
|
|
4050
4431
|
}
|
|
4051
4432
|
const m = declMatch ?? assignMatch;
|
|
4052
4433
|
if (!m)
|
|
4053
4434
|
continue;
|
|
4054
4435
|
const lhs = m[1];
|
|
4055
|
-
|
|
4436
|
+
let rhs = m[2];
|
|
4056
4437
|
if (JAVA_KEYWORDS.has(lhs))
|
|
4057
4438
|
continue;
|
|
4058
4439
|
if (knownTainted.has(lhs))
|
|
4059
4440
|
continue;
|
|
4441
|
+
if (trackSlots) {
|
|
4442
|
+
const slot = javaSlotRead(rhs, listElems, mapSlots);
|
|
4443
|
+
if (slot !== undefined) {
|
|
4444
|
+
if (slot.some(t => knownTainted.has(t))) {
|
|
4445
|
+
derived.set(lhs, i + 1);
|
|
4446
|
+
knownTainted.add(lhs);
|
|
4447
|
+
changed = true;
|
|
4448
|
+
}
|
|
4449
|
+
continue;
|
|
4450
|
+
}
|
|
4451
|
+
const tern = /^([^?]+?)\s*\?\s*([^:]+?)\s*:\s*(.+)$/.exec(rhs.trim());
|
|
4452
|
+
if (tern) {
|
|
4453
|
+
const decided = evaluatePythonConstExpression(tern[1].trim().replace(/^\((.*)\)$/, '$1'), intConsts);
|
|
4454
|
+
if (decided === true)
|
|
4455
|
+
rhs = tern[2];
|
|
4456
|
+
else if (decided === false)
|
|
4457
|
+
rhs = tern[3];
|
|
4458
|
+
}
|
|
4459
|
+
}
|
|
4060
4460
|
// Reduce the RHS to code positions before matching a tainted var name:
|
|
4061
4461
|
// keep `$"…{expr}"` interpolation contents, drop plain / verbatim string
|
|
4062
4462
|
// literal bodies and char literals. Otherwise a tainted var NAME that
|
|
@@ -4152,8 +4552,15 @@ function findBashTaintSources(sourceCode, dfg) {
|
|
|
4152
4552
|
// Suppressed inside function bodies (#73 part 1).
|
|
4153
4553
|
if (!insideFunction) {
|
|
4154
4554
|
const positionalRe = /\$([1-9@*])|\$\{([1-9@*])\}/g;
|
|
4555
|
+
// A one-line definition `name() { …; }` keeps its body on the header
|
|
4556
|
+
// line: only text before the opening brace is at the top level.
|
|
4557
|
+
const header = /^\s*(?:function\s+)?[A-Za-z_][\w-]*\s*(?:\(\s*\))?\s*\{/.exec(line);
|
|
4558
|
+
const isFnHeader = header !== null && (/\(\s*\)/.test(header[0]) || /^\s*function\s/.test(header[0]));
|
|
4559
|
+
// `$1` inside single quotes is literal text (an awk or sed program), not
|
|
4560
|
+
// a positional parameter.
|
|
4561
|
+
const topLevelText = (isFnHeader ? line.slice(0, header[0].length) : line).replace(/'[^']*'/g, "''");
|
|
4155
4562
|
let m;
|
|
4156
|
-
while ((m = positionalRe.exec(
|
|
4563
|
+
while ((m = positionalRe.exec(topLevelText)) !== null) {
|
|
4157
4564
|
const param = m[1] ?? m[2];
|
|
4158
4565
|
const alreadyExists = sources.some(s => s.line === lineNumber && s.variable === param);
|
|
4159
4566
|
if (!alreadyExists) {
|
|
@@ -4893,6 +5300,39 @@ function findBashRealpathPrefixGuardSanitizers(code) {
|
|
|
4893
5300
|
}
|
|
4894
5301
|
return sanitizers;
|
|
4895
5302
|
}
|
|
5303
|
+
/**
|
|
5304
|
+
* cognium-dev #816 (CVE-2026-8398 `daemon_pkg_install` safe mirror): when a
|
|
5305
|
+
* script downloads a package artifact with curl/wget and later verifies
|
|
5306
|
+
* integrity (gpg/sha) before `dpkg`/`rpm` install, credit the download
|
|
5307
|
+
* step's outbound fetch as gated — same whole-script idiom as
|
|
5308
|
+
* `hasIntegrityVerifierAnywhere` for `unverified-package-install`.
|
|
5309
|
+
*/
|
|
5310
|
+
function findBashVerifiedPackageDownloadSanitizers(code) {
|
|
5311
|
+
const lines = code.split('\n');
|
|
5312
|
+
if (!hasIntegrityVerifierAnywhere(lines)) {
|
|
5313
|
+
return [];
|
|
5314
|
+
}
|
|
5315
|
+
const hasDownload = lines.some((l) => /\b(curl|wget)\b/.test(l));
|
|
5316
|
+
const hasInstall = lines.some((l) => matchUnverifiedPackageInstall(l.trim()) !== null);
|
|
5317
|
+
if (!hasDownload || !hasInstall) {
|
|
5318
|
+
return [];
|
|
5319
|
+
}
|
|
5320
|
+
const sanitizers = [];
|
|
5321
|
+
for (let i = 0; i < lines.length; i++) {
|
|
5322
|
+
const line = lines[i];
|
|
5323
|
+
if (line.trimStart().startsWith('#'))
|
|
5324
|
+
continue;
|
|
5325
|
+
if (!/\b(curl|wget)\b/.test(line))
|
|
5326
|
+
continue;
|
|
5327
|
+
sanitizers.push({
|
|
5328
|
+
type: 'verified_package_download',
|
|
5329
|
+
method: 'curl',
|
|
5330
|
+
line: i + 1,
|
|
5331
|
+
sanitizes: ['ssrf'],
|
|
5332
|
+
});
|
|
5333
|
+
}
|
|
5334
|
+
return sanitizers;
|
|
5335
|
+
}
|
|
4896
5336
|
// ---------------------------------------------------------------------------
|
|
4897
5337
|
// Go safe-handler sanitizer detectors (cognium-dev #102 Sprint 24)
|
|
4898
5338
|
// ---------------------------------------------------------------------------
|
|
@@ -5194,6 +5634,208 @@ function findPythonNetlocAllowlistGuardSanitizers(code) {
|
|
|
5194
5634
|
* or UPPER_SNAKE constants (e.g. MAX_QTY). The guard body must contain
|
|
5195
5635
|
* a terminator.
|
|
5196
5636
|
*/
|
|
5637
|
+
/**
|
|
5638
|
+
* JS/TS numeric range-check guards on Number/parseInt-coerced values
|
|
5639
|
+
* (cognium-dev #816, PAT-INPUT-01 safe mirror; Python twin #114).
|
|
5640
|
+
*
|
|
5641
|
+
* const qty = parseInt(req.query.qty, 10);
|
|
5642
|
+
* if (!Number.isFinite(qty) || qty <= 0) return res.status(400).end();
|
|
5643
|
+
* if (qty > MAX) return res.status(400).end();
|
|
5644
|
+
* res.end(`xfer ${qty}`);
|
|
5645
|
+
*
|
|
5646
|
+
* Coercion strips xss in DEFAULT_SANITIZERS but template interpolation
|
|
5647
|
+
* re-taints; a proven finite + bounded range guard shows the emitted
|
|
5648
|
+
* scalar cannot carry markup.
|
|
5649
|
+
*/
|
|
5650
|
+
function findJsNumericRangeCheckGuardSanitizers(code) {
|
|
5651
|
+
const sanitizers = [];
|
|
5652
|
+
const lines = code.split('\n');
|
|
5653
|
+
const coerceDecl = /^\s*(?:const|let|var)\s+([A-Za-z_]\w*)\s*=\s*(?:Number|parseInt)\s*\(/;
|
|
5654
|
+
const lowerGuard = /\bif\s*\(\s*!(?:Number\.)?isFinite\s*\(\s*([A-Za-z_]\w*)\s*\)|\bif\s*\([^)]*\b([A-Za-z_]\w*)\s*<=\s*0/;
|
|
5655
|
+
const upperGuard = /\bif\s*\(\s*([A-Za-z_]\w*)\s*>\s*(?:\d+|[A-Z][A-Z0-9_]+)\s*\)/;
|
|
5656
|
+
const terminator = /\b(?:return|throw|res\s*\.\s*status\s*\([^)]*\)\s*\.\s*(?:send|end|json))/;
|
|
5657
|
+
const coerced = new Map();
|
|
5658
|
+
for (let i = 0; i < lines.length; i++) {
|
|
5659
|
+
const m = coerceDecl.exec(lines[i]);
|
|
5660
|
+
if (m)
|
|
5661
|
+
coerced.set(m[1], i);
|
|
5662
|
+
}
|
|
5663
|
+
if (coerced.size === 0)
|
|
5664
|
+
return sanitizers;
|
|
5665
|
+
const state = new Map();
|
|
5666
|
+
for (let i = 0; i < lines.length; i++) {
|
|
5667
|
+
const line = lines[i];
|
|
5668
|
+
let varName = null;
|
|
5669
|
+
let kind = null;
|
|
5670
|
+
const low = lowerGuard.exec(line);
|
|
5671
|
+
if (low) {
|
|
5672
|
+
varName = low[1] ?? low[2];
|
|
5673
|
+
kind = 'lower';
|
|
5674
|
+
}
|
|
5675
|
+
else {
|
|
5676
|
+
const up = upperGuard.exec(line);
|
|
5677
|
+
if (up) {
|
|
5678
|
+
varName = up[1];
|
|
5679
|
+
kind = 'upper';
|
|
5680
|
+
}
|
|
5681
|
+
}
|
|
5682
|
+
if (!varName || !kind || !coerced.has(varName))
|
|
5683
|
+
continue;
|
|
5684
|
+
if (!terminator.test(line)) {
|
|
5685
|
+
let bodyHasTerminator = false;
|
|
5686
|
+
let blockEnd = i;
|
|
5687
|
+
let braceDepth = 0;
|
|
5688
|
+
let started = false;
|
|
5689
|
+
const maxScan = Math.min(lines.length, i + 26);
|
|
5690
|
+
for (let j = i; j < maxScan; j++) {
|
|
5691
|
+
const ln = lines[j];
|
|
5692
|
+
for (const ch of ln) {
|
|
5693
|
+
if (ch === '{') {
|
|
5694
|
+
braceDepth++;
|
|
5695
|
+
started = true;
|
|
5696
|
+
}
|
|
5697
|
+
else if (ch === '}') {
|
|
5698
|
+
braceDepth--;
|
|
5699
|
+
if (started && braceDepth === 0) {
|
|
5700
|
+
blockEnd = j;
|
|
5701
|
+
break;
|
|
5702
|
+
}
|
|
5703
|
+
}
|
|
5704
|
+
}
|
|
5705
|
+
if (j > i && terminator.test(ln))
|
|
5706
|
+
bodyHasTerminator = true;
|
|
5707
|
+
if (started && braceDepth === 0 && blockEnd !== i)
|
|
5708
|
+
break;
|
|
5709
|
+
}
|
|
5710
|
+
if (!bodyHasTerminator && !started)
|
|
5711
|
+
continue;
|
|
5712
|
+
if (!bodyHasTerminator)
|
|
5713
|
+
continue;
|
|
5714
|
+
}
|
|
5715
|
+
const st = state.get(varName) ?? { lower: false, upper: false, lastGuardEnd: i };
|
|
5716
|
+
if (kind === 'lower')
|
|
5717
|
+
st.lower = true;
|
|
5718
|
+
else
|
|
5719
|
+
st.upper = true;
|
|
5720
|
+
st.lastGuardEnd = i;
|
|
5721
|
+
state.set(varName, st);
|
|
5722
|
+
}
|
|
5723
|
+
for (const [varName, st] of state) {
|
|
5724
|
+
if (!st.lower || !st.upper)
|
|
5725
|
+
continue;
|
|
5726
|
+
const varRefRe = new RegExp(`\\b${varName}\\b`);
|
|
5727
|
+
for (let l = st.lastGuardEnd + 1; l < lines.length; l++) {
|
|
5728
|
+
if (!varRefRe.test(lines[l]))
|
|
5729
|
+
continue;
|
|
5730
|
+
sanitizers.push({
|
|
5731
|
+
type: 'js_numeric_range_check_guard',
|
|
5732
|
+
method: 'if',
|
|
5733
|
+
line: l + 1,
|
|
5734
|
+
sanitizes: ['xss', 'external_taint_escape'],
|
|
5735
|
+
});
|
|
5736
|
+
}
|
|
5737
|
+
}
|
|
5738
|
+
return sanitizers;
|
|
5739
|
+
}
|
|
5740
|
+
/**
|
|
5741
|
+
* Rust numeric range-check guards on `.parse()`-coerced values
|
|
5742
|
+
* (cognium-dev #816, PAT-INPUT-01 safe mirror).
|
|
5743
|
+
*
|
|
5744
|
+
* let amount: i32 = amount_str.parse().unwrap_or(0);
|
|
5745
|
+
* if amount <= 0 { return HttpResponse::BadRequest().finish(); }
|
|
5746
|
+
* if amount > MAX { return HttpResponse::BadRequest().finish(); }
|
|
5747
|
+
* HttpResponse::Ok().body(format!("xfer {}", amount))
|
|
5748
|
+
*/
|
|
5749
|
+
function findRustNumericRangeCheckGuardSanitizers(code) {
|
|
5750
|
+
const sanitizers = [];
|
|
5751
|
+
const lines = code.split('\n');
|
|
5752
|
+
const coerceDecl = /^\s*let\s+([A-Za-z_]\w*)\s*(?::\s*(?:i\d+|u\d+|isize|usize))?\s*=\s*[^;]*\.parse\s*(?:\(\s*\)|::<)/;
|
|
5753
|
+
const lowerGuard = /\bif\s+([A-Za-z_]\w*)\s*<=\s*0\b/;
|
|
5754
|
+
const upperGuard = /\bif\s+([A-Za-z_]\w*)\s*>\s*(?:\d+|[A-Z][A-Z0-9_]+)\b/;
|
|
5755
|
+
const terminator = /\b(?:return\s+HttpResponse::|return\s+StatusCode::|return\s+Err\s*\(|panic!\s*\()/;
|
|
5756
|
+
const coerced = new Map();
|
|
5757
|
+
for (let i = 0; i < lines.length; i++) {
|
|
5758
|
+
const m = coerceDecl.exec(lines[i]);
|
|
5759
|
+
if (m)
|
|
5760
|
+
coerced.set(m[1], i);
|
|
5761
|
+
}
|
|
5762
|
+
if (coerced.size === 0)
|
|
5763
|
+
return sanitizers;
|
|
5764
|
+
const state = new Map();
|
|
5765
|
+
for (let i = 0; i < lines.length; i++) {
|
|
5766
|
+
const line = lines[i];
|
|
5767
|
+
let varName = null;
|
|
5768
|
+
let kind = null;
|
|
5769
|
+
const low = lowerGuard.exec(line);
|
|
5770
|
+
if (low) {
|
|
5771
|
+
varName = low[1];
|
|
5772
|
+
kind = 'lower';
|
|
5773
|
+
}
|
|
5774
|
+
else {
|
|
5775
|
+
const up = upperGuard.exec(line);
|
|
5776
|
+
if (up) {
|
|
5777
|
+
varName = up[1];
|
|
5778
|
+
kind = 'upper';
|
|
5779
|
+
}
|
|
5780
|
+
}
|
|
5781
|
+
if (!varName || !kind || !coerced.has(varName))
|
|
5782
|
+
continue;
|
|
5783
|
+
if (!terminator.test(line)) {
|
|
5784
|
+
let bodyHasTerminator = false;
|
|
5785
|
+
let blockEnd = i;
|
|
5786
|
+
let braceDepth = 0;
|
|
5787
|
+
let started = false;
|
|
5788
|
+
const maxScan = Math.min(lines.length, i + 26);
|
|
5789
|
+
for (let j = i; j < maxScan; j++) {
|
|
5790
|
+
const ln = lines[j];
|
|
5791
|
+
for (const ch of ln) {
|
|
5792
|
+
if (ch === '{') {
|
|
5793
|
+
braceDepth++;
|
|
5794
|
+
started = true;
|
|
5795
|
+
}
|
|
5796
|
+
else if (ch === '}') {
|
|
5797
|
+
braceDepth--;
|
|
5798
|
+
if (started && braceDepth === 0) {
|
|
5799
|
+
blockEnd = j;
|
|
5800
|
+
break;
|
|
5801
|
+
}
|
|
5802
|
+
}
|
|
5803
|
+
}
|
|
5804
|
+
if (j > i && terminator.test(ln))
|
|
5805
|
+
bodyHasTerminator = true;
|
|
5806
|
+
if (started && braceDepth === 0 && blockEnd !== i)
|
|
5807
|
+
break;
|
|
5808
|
+
}
|
|
5809
|
+
if (!bodyHasTerminator && !started)
|
|
5810
|
+
continue;
|
|
5811
|
+
if (!bodyHasTerminator)
|
|
5812
|
+
continue;
|
|
5813
|
+
}
|
|
5814
|
+
const st = state.get(varName) ?? { lower: false, upper: false, lastGuardEnd: i };
|
|
5815
|
+
if (kind === 'lower')
|
|
5816
|
+
st.lower = true;
|
|
5817
|
+
else
|
|
5818
|
+
st.upper = true;
|
|
5819
|
+
st.lastGuardEnd = i;
|
|
5820
|
+
state.set(varName, st);
|
|
5821
|
+
}
|
|
5822
|
+
for (const [varName, st] of state) {
|
|
5823
|
+
if (!st.lower || !st.upper)
|
|
5824
|
+
continue;
|
|
5825
|
+
const varRefRe = new RegExp(`\\b${varName}\\b`);
|
|
5826
|
+
for (let l = st.lastGuardEnd + 1; l < lines.length; l++) {
|
|
5827
|
+
if (!varRefRe.test(lines[l]))
|
|
5828
|
+
continue;
|
|
5829
|
+
sanitizers.push({
|
|
5830
|
+
type: 'rust_numeric_range_check_guard',
|
|
5831
|
+
method: 'if',
|
|
5832
|
+
line: l + 1,
|
|
5833
|
+
sanitizes: ['xss', 'external_taint_escape'],
|
|
5834
|
+
});
|
|
5835
|
+
}
|
|
5836
|
+
}
|
|
5837
|
+
return sanitizers;
|
|
5838
|
+
}
|
|
5197
5839
|
function findPythonRangeCheckGuardSanitizers(code) {
|
|
5198
5840
|
const sanitizers = [];
|
|
5199
5841
|
const lines = code.split('\n');
|
|
@@ -6062,6 +6704,161 @@ function findPythonDefusedXmlSanitizers(code) {
|
|
|
6062
6704
|
}
|
|
6063
6705
|
return sanitizers;
|
|
6064
6706
|
}
|
|
6707
|
+
/**
|
|
6708
|
+
* PAT-INFO-01 safe mirror (cognium-dev #816): `HttpResponse` body built with
|
|
6709
|
+
* `format!` interpolates only `web::Query` / `.get(...)` values — no `env::var`.
|
|
6710
|
+
* Credits the sink so CWE-200-style response leaks stay on env-backed TP controls.
|
|
6711
|
+
*/
|
|
6712
|
+
function findRustPatInfoHttpOnlyResponseSanitizers(code) {
|
|
6713
|
+
const sanitizers = [];
|
|
6714
|
+
const lines = code.split('\n');
|
|
6715
|
+
const bodyFormatRe = /HttpResponse::\w+\(\)\s*\.\s*body\s*\(\s*format!\s*\(/;
|
|
6716
|
+
const envBindRe = /(?:^|[^:])env::var\s*\(|std::env::/;
|
|
6717
|
+
const httpGetRe = /\.get\s*\(\s*"[^"]+"\s*\)/;
|
|
6718
|
+
const letVarRe = (v) => new RegExp(String.raw `^\s*let\s+(?:mut\s+)?${v}\s*=`);
|
|
6719
|
+
for (let i = 0; i < lines.length; i++) {
|
|
6720
|
+
const sinkLine = lines[i];
|
|
6721
|
+
if (!bodyFormatRe.test(sinkLine))
|
|
6722
|
+
continue;
|
|
6723
|
+
if (envBindRe.test(sinkLine))
|
|
6724
|
+
continue;
|
|
6725
|
+
const varRefs = [
|
|
6726
|
+
...sinkLine.matchAll(/\{([A-Za-z_][A-Za-z0-9_]*)\}/g),
|
|
6727
|
+
].map((m) => m[1]);
|
|
6728
|
+
if (varRefs.length === 0)
|
|
6729
|
+
continue;
|
|
6730
|
+
let allHttpOnly = true;
|
|
6731
|
+
for (const v of varRefs) {
|
|
6732
|
+
let sawBinding = false;
|
|
6733
|
+
let boundEnv = false;
|
|
6734
|
+
let boundHttp = false;
|
|
6735
|
+
for (let j = i - 1; j >= Math.max(0, i - 24); j--) {
|
|
6736
|
+
const line = lines[j];
|
|
6737
|
+
if (!letVarRe(v).test(line))
|
|
6738
|
+
continue;
|
|
6739
|
+
sawBinding = true;
|
|
6740
|
+
if (envBindRe.test(line))
|
|
6741
|
+
boundEnv = true;
|
|
6742
|
+
if (httpGetRe.test(line))
|
|
6743
|
+
boundHttp = true;
|
|
6744
|
+
break;
|
|
6745
|
+
}
|
|
6746
|
+
if (!sawBinding || boundEnv || !boundHttp) {
|
|
6747
|
+
allHttpOnly = false;
|
|
6748
|
+
break;
|
|
6749
|
+
}
|
|
6750
|
+
}
|
|
6751
|
+
if (!allHttpOnly)
|
|
6752
|
+
continue;
|
|
6753
|
+
sanitizers.push({
|
|
6754
|
+
type: 'rust_pat_info_http_only_body',
|
|
6755
|
+
method: 'format',
|
|
6756
|
+
line: i + 1,
|
|
6757
|
+
sanitizes: [
|
|
6758
|
+
'xss',
|
|
6759
|
+
'external_taint_escape',
|
|
6760
|
+
'open_redirect',
|
|
6761
|
+
'log_injection',
|
|
6762
|
+
],
|
|
6763
|
+
});
|
|
6764
|
+
}
|
|
6765
|
+
return sanitizers;
|
|
6766
|
+
}
|
|
6767
|
+
/**
|
|
6768
|
+
* Rust `const SLICE: &[&str] = &["a", "b"]` membership reject guard
|
|
6769
|
+
* (cognium-dev #816 PAT-TRUST-01; Python twin #563).
|
|
6770
|
+
*
|
|
6771
|
+
* if !THEMES.contains(&theme.as_str()) {
|
|
6772
|
+
* return HttpResponse::BadRequest().finish();
|
|
6773
|
+
* }
|
|
6774
|
+
* session.insert("theme", theme)
|
|
6775
|
+
*/
|
|
6776
|
+
function collectRustConstStrSliceNames(code) {
|
|
6777
|
+
const names = new Set();
|
|
6778
|
+
const re = /^\s*const\s+([A-Z][A-Z0-9_]*)\s*:\s*&\s*\[\s*&\s*str\s*\]\s*=\s*&\s*\[([^\]]*)\]\s*;/;
|
|
6779
|
+
for (const line of code.split('\n')) {
|
|
6780
|
+
const m = re.exec(line);
|
|
6781
|
+
if (!m)
|
|
6782
|
+
continue;
|
|
6783
|
+
const inner = m[2].trim();
|
|
6784
|
+
if (!inner) {
|
|
6785
|
+
names.add(m[1]);
|
|
6786
|
+
continue;
|
|
6787
|
+
}
|
|
6788
|
+
const elems = inner.split(',').map(s => s.trim()).filter(Boolean);
|
|
6789
|
+
if (elems.every(e => /^"(?:[^"\\]|\\.)*"$/.test(e)))
|
|
6790
|
+
names.add(m[1]);
|
|
6791
|
+
}
|
|
6792
|
+
return names;
|
|
6793
|
+
}
|
|
6794
|
+
function findRustConstSliceMembershipGuardSanitizers(code) {
|
|
6795
|
+
const sanitizers = [];
|
|
6796
|
+
const slices = collectRustConstStrSliceNames(code);
|
|
6797
|
+
if (slices.size === 0)
|
|
6798
|
+
return sanitizers;
|
|
6799
|
+
const lines = code.split('\n');
|
|
6800
|
+
const guardOpen = /^\s*if\s+!\s*([A-Z][A-Z0-9_]*)\s*\.\s*contains\s*\(\s*&\s*([A-Za-z_]\w*)(?:\s*\.\s*as_str\s*\(\s*\))?\s*\)\s*\{?\s*$/;
|
|
6801
|
+
const terminator = /\b(?:return\s+HttpResponse::|return\s+StatusCode::|return\s+Err\s*\(|panic!\s*\()/;
|
|
6802
|
+
for (let i = 0; i < lines.length; i++) {
|
|
6803
|
+
const m = guardOpen.exec(lines[i]);
|
|
6804
|
+
if (!m)
|
|
6805
|
+
continue;
|
|
6806
|
+
const sliceName = m[1];
|
|
6807
|
+
const guardedVar = m[2];
|
|
6808
|
+
if (!slices.has(sliceName))
|
|
6809
|
+
continue;
|
|
6810
|
+
let depth = 0;
|
|
6811
|
+
for (const ch of lines[i]) {
|
|
6812
|
+
if (ch === '{')
|
|
6813
|
+
depth++;
|
|
6814
|
+
else if (ch === '}')
|
|
6815
|
+
depth--;
|
|
6816
|
+
}
|
|
6817
|
+
let closeLine = i;
|
|
6818
|
+
let bodyHasTerminator = terminator.test(lines[i]);
|
|
6819
|
+
if (depth > 0) {
|
|
6820
|
+
for (let j = i + 1; j < Math.min(lines.length, i + 26); j++) {
|
|
6821
|
+
if (terminator.test(lines[j]))
|
|
6822
|
+
bodyHasTerminator = true;
|
|
6823
|
+
for (const ch of lines[j]) {
|
|
6824
|
+
if (ch === '{')
|
|
6825
|
+
depth++;
|
|
6826
|
+
else if (ch === '}')
|
|
6827
|
+
depth--;
|
|
6828
|
+
}
|
|
6829
|
+
if (depth === 0) {
|
|
6830
|
+
closeLine = j;
|
|
6831
|
+
break;
|
|
6832
|
+
}
|
|
6833
|
+
}
|
|
6834
|
+
}
|
|
6835
|
+
if (!bodyHasTerminator)
|
|
6836
|
+
continue;
|
|
6837
|
+
const varRe = new RegExp(String.raw `\b${guardedVar}\b`);
|
|
6838
|
+
const reassign = new RegExp(String.raw `^\s*(?:let\s+)?(?:mut\s+)?${guardedVar}\s*=(?!=)`);
|
|
6839
|
+
for (let l = closeLine + 2; l <= lines.length; l++) {
|
|
6840
|
+
const lineText = lines[l - 1];
|
|
6841
|
+
if (!lineText || !varRe.test(lineText))
|
|
6842
|
+
continue;
|
|
6843
|
+
if (reassign.test(lineText))
|
|
6844
|
+
break;
|
|
6845
|
+
sanitizers.push({
|
|
6846
|
+
type: 'rust_const_slice_membership_guard',
|
|
6847
|
+
method: 'if',
|
|
6848
|
+
line: l,
|
|
6849
|
+
sanitizes: [
|
|
6850
|
+
'trust_boundary',
|
|
6851
|
+
'external_taint_escape',
|
|
6852
|
+
'xss',
|
|
6853
|
+
'open_redirect',
|
|
6854
|
+
'ssrf',
|
|
6855
|
+
'command_injection',
|
|
6856
|
+
],
|
|
6857
|
+
});
|
|
6858
|
+
}
|
|
6859
|
+
}
|
|
6860
|
+
return sanitizers;
|
|
6861
|
+
}
|
|
6065
6862
|
/**
|
|
6066
6863
|
* Detect Rust HashSet/HashMap host allow-list guards (cognium-dev #115 FP-23).
|
|
6067
6864
|
*
|
|
@@ -6333,6 +7130,16 @@ function findJsCsvFormulaPrefixSanitizers(code) {
|
|
|
6333
7130
|
* threat-set. Custom wrappers using different escape strategies (e.g.
|
|
6334
7131
|
* URL encode) are not yet covered.
|
|
6335
7132
|
*/
|
|
7133
|
+
/** Return expression of a small JS wrapper (function or arrow) for #562 chain verdict. */
|
|
7134
|
+
function jsWrapperBodyReplaceChainText(body) {
|
|
7135
|
+
const returnM = /\breturn\s+([^;}\n]+)/.exec(body);
|
|
7136
|
+
if (returnM)
|
|
7137
|
+
return returnM[1].trim();
|
|
7138
|
+
const arrowM = /=>\s*([^;}\n]+)/.exec(body);
|
|
7139
|
+
if (arrowM)
|
|
7140
|
+
return arrowM[1].trim();
|
|
7141
|
+
return null;
|
|
7142
|
+
}
|
|
6336
7143
|
function findJsWrapperFunctionSanitizers(code) {
|
|
6337
7144
|
const sanitizers = [];
|
|
6338
7145
|
const lines = code.split('\n');
|
|
@@ -6355,8 +7162,13 @@ function findJsWrapperFunctionSanitizers(code) {
|
|
|
6355
7162
|
kinds.add('external_taint_escape');
|
|
6356
7163
|
}
|
|
6357
7164
|
if (crlfCharClass.test(body)) {
|
|
6358
|
-
|
|
6359
|
-
|
|
7165
|
+
const chainText = jsWrapperBodyReplaceChainText(body);
|
|
7166
|
+
const verdict = chainText ? jsReplaceChainVerdict(chainText) : 'undecided';
|
|
7167
|
+
// #562 / #605: LF-only or CRLF-pair-only strips must not credit log_injection.
|
|
7168
|
+
if (verdict !== 'leaves-crlf') {
|
|
7169
|
+
kinds.add('log_injection');
|
|
7170
|
+
kinds.add('external_taint_escape');
|
|
7171
|
+
}
|
|
6360
7172
|
}
|
|
6361
7173
|
if (kinds.size > 0)
|
|
6362
7174
|
wrappers.set(name, kinds);
|
|
@@ -7765,6 +8577,87 @@ function javaBodyStripsCrAndLf(body) {
|
|
|
7765
8577
|
}
|
|
7766
8578
|
return cr && lf;
|
|
7767
8579
|
}
|
|
8580
|
+
/** True when the helper removes both `{` and `}` via replace/replaceAll (flow-sanitize ssti). */
|
|
8581
|
+
function javaBodyStripsTemplateDelimiters(body) {
|
|
8582
|
+
let open = false;
|
|
8583
|
+
let close = false;
|
|
8584
|
+
for (const m of body.matchAll(/\.\s*(replace|replaceAll)\s*\(\s*(?:"((?:\\.|[^"\\])*)"|'(\\.|[^'\\])')/g)) {
|
|
8585
|
+
const lit = m[2] ?? m[3] ?? '';
|
|
8586
|
+
if (lit === '{')
|
|
8587
|
+
open = true;
|
|
8588
|
+
if (lit === '}')
|
|
8589
|
+
close = true;
|
|
8590
|
+
}
|
|
8591
|
+
return open && close;
|
|
8592
|
+
}
|
|
8593
|
+
/**
|
|
8594
|
+
* Java user-defined wrapper that strips Velocity/Jinja-style `{` `}` delimiters.
|
|
8595
|
+
* Mirrors flow-sanitize `ssti` / `custom_wrapper` safe mirrors (cognium-dev#947).
|
|
8596
|
+
*/
|
|
8597
|
+
function findJavaTemplateDelimiterWrapperSanitizers(code) {
|
|
8598
|
+
const sanitizers = [];
|
|
8599
|
+
const lines = code.split('\n');
|
|
8600
|
+
const declRe = /^\s*(?:public|private|protected)?\s*(?:static\s+)?(?:final\s+)?(?:String|CharSequence)\s+([A-Za-z_]\w*)\s*\(/;
|
|
8601
|
+
const bodyWindow = 10;
|
|
8602
|
+
const wrappers = new Set();
|
|
8603
|
+
for (let i = 0; i < lines.length; i++) {
|
|
8604
|
+
const m = declRe.exec(lines[i]);
|
|
8605
|
+
if (!m)
|
|
8606
|
+
continue;
|
|
8607
|
+
let end = Math.min(lines.length, i + bodyWindow);
|
|
8608
|
+
for (let j = i + 1; j < end; j++) {
|
|
8609
|
+
if (declRe.test(lines[j])) {
|
|
8610
|
+
end = j;
|
|
8611
|
+
break;
|
|
8612
|
+
}
|
|
8613
|
+
}
|
|
8614
|
+
const body = lines.slice(i, end).join('\n');
|
|
8615
|
+
if (javaBodyStripsTemplateDelimiters(body))
|
|
8616
|
+
wrappers.add(m[1]);
|
|
8617
|
+
}
|
|
8618
|
+
if (wrappers.size === 0)
|
|
8619
|
+
return sanitizers;
|
|
8620
|
+
const kinds = ['code_injection', 'external_taint_escape'];
|
|
8621
|
+
for (const name of wrappers) {
|
|
8622
|
+
const assignedCallRe = new RegExp(`\\b([A-Za-z_][A-Za-z0-9_]*)\\s*=\\s*${name}\\s*\\(`);
|
|
8623
|
+
const bareCallRe = new RegExp(`\\b${name}\\s*\\(`);
|
|
8624
|
+
for (let i = 0; i < lines.length; i++) {
|
|
8625
|
+
if (!bareCallRe.test(lines[i]))
|
|
8626
|
+
continue;
|
|
8627
|
+
if (declRe.test(lines[i]) && bareCallRe.test(lines[i]))
|
|
8628
|
+
continue;
|
|
8629
|
+
sanitizers.push({
|
|
8630
|
+
type: 'java_wrapper_template_delim_strip',
|
|
8631
|
+
method: name,
|
|
8632
|
+
line: i + 1,
|
|
8633
|
+
sanitizes: [...kinds],
|
|
8634
|
+
});
|
|
8635
|
+
const am = assignedCallRe.exec(lines[i]);
|
|
8636
|
+
if (!am)
|
|
8637
|
+
continue;
|
|
8638
|
+
const callIndent = lines[i].length - lines[i].trimStart().length;
|
|
8639
|
+
const validated = am[1];
|
|
8640
|
+
const validatedRe = new RegExp(`\\b${validated}\\b`);
|
|
8641
|
+
for (let j = i + 1; j < lines.length; j++) {
|
|
8642
|
+
const jline = lines[j];
|
|
8643
|
+
if (jline.trim() === '')
|
|
8644
|
+
continue;
|
|
8645
|
+
const jindent = jline.length - jline.trimStart().length;
|
|
8646
|
+
if (jindent < callIndent)
|
|
8647
|
+
break;
|
|
8648
|
+
if (validatedRe.test(jline)) {
|
|
8649
|
+
sanitizers.push({
|
|
8650
|
+
type: 'java_wrapper_template_delim_strip',
|
|
8651
|
+
method: name,
|
|
8652
|
+
line: j + 1,
|
|
8653
|
+
sanitizes: [...kinds],
|
|
8654
|
+
});
|
|
8655
|
+
}
|
|
8656
|
+
}
|
|
8657
|
+
}
|
|
8658
|
+
}
|
|
8659
|
+
return sanitizers;
|
|
8660
|
+
}
|
|
7768
8661
|
function findCrlfWrapperFunctionSanitizers(code, language) {
|
|
7769
8662
|
const sanitizers = [];
|
|
7770
8663
|
const lines = code.split('\n');
|
|
@@ -11196,6 +12089,108 @@ export function findJsLdapInjectionFindings(code, file) {
|
|
|
11196
12089
|
snippet: line.trim(),
|
|
11197
12090
|
});
|
|
11198
12091
|
}
|
|
12092
|
+
// cognium-dev #899: ldapjs also accepts the filter as the 2nd positional
|
|
12093
|
+
// argument (`client.search(base, '(mail=' + email + ')', cb)`), not only
|
|
12094
|
+
// as `opts.filter`. Flag when that argument is built from tainted data.
|
|
12095
|
+
// Base DN is often comma-separated; only match a quoted first argument.
|
|
12096
|
+
const positionalSearchRe = /\.\s*search\s*\(\s*(?:'[^']*'|"[^"]*")\s*,\s*([^,{]+)/;
|
|
12097
|
+
for (let i = 0; i < lines.length; i++) {
|
|
12098
|
+
const line = lines[i];
|
|
12099
|
+
const pm = line.match(positionalSearchRe);
|
|
12100
|
+
if (!pm)
|
|
12101
|
+
continue;
|
|
12102
|
+
const expr = pm[1].trim();
|
|
12103
|
+
if (expr.startsWith('{'))
|
|
12104
|
+
continue;
|
|
12105
|
+
let tainted = false;
|
|
12106
|
+
for (const v of taintedVars) {
|
|
12107
|
+
if (new RegExp(`\\b${v}\\b`).test(expr)) {
|
|
12108
|
+
tainted = true;
|
|
12109
|
+
break;
|
|
12110
|
+
}
|
|
12111
|
+
}
|
|
12112
|
+
if (!tainted)
|
|
12113
|
+
continue;
|
|
12114
|
+
const lineNo = i + 1;
|
|
12115
|
+
if (seen.has(lineNo))
|
|
12116
|
+
continue;
|
|
12117
|
+
seen.add(lineNo);
|
|
12118
|
+
findings.push({
|
|
12119
|
+
id: `ldap_injection-${file}-${lineNo}-js-ldap-positional-filter`,
|
|
12120
|
+
pass: 'language-sources',
|
|
12121
|
+
category: 'security',
|
|
12122
|
+
rule_id: 'ldap_injection',
|
|
12123
|
+
cwe: 'CWE-90',
|
|
12124
|
+
severity: 'critical',
|
|
12125
|
+
level: 'error',
|
|
12126
|
+
message: 'LDAP injection: ldapjs/ldapts `client.search(base, <tainted-filter>, ...)` ' +
|
|
12127
|
+
'lets the attacker break out of the filter expression. Escape user input ' +
|
|
12128
|
+
'or use a structured filter builder.',
|
|
12129
|
+
file,
|
|
12130
|
+
line: lineNo,
|
|
12131
|
+
snippet: line.trim(),
|
|
12132
|
+
});
|
|
12133
|
+
}
|
|
12134
|
+
return findings;
|
|
12135
|
+
}
|
|
12136
|
+
/**
|
|
12137
|
+
* cognium-dev #897 — ZIP local-file-header validation that fail-opens: after
|
|
12138
|
+
* rejecting an invalid PK\\x03\\x04 signature, returning a slice of the caller
|
|
12139
|
+
* buffer instead of throwing lets untrusted bytes through (CWE-20).
|
|
12140
|
+
*/
|
|
12141
|
+
export function findJsZipHeaderFailopenFindings(code, file) {
|
|
12142
|
+
const findings = [];
|
|
12143
|
+
if (typeof code !== 'string' || code.length === 0)
|
|
12144
|
+
return findings;
|
|
12145
|
+
const lines = code.split('\n');
|
|
12146
|
+
const hasPkLocalSig = lines.some((l) => {
|
|
12147
|
+
const t = l.trim();
|
|
12148
|
+
if (t.startsWith('//'))
|
|
12149
|
+
return false;
|
|
12150
|
+
return (/0x50/.test(t) &&
|
|
12151
|
+
/0x4[bB]/.test(t) &&
|
|
12152
|
+
/0x03/.test(t) &&
|
|
12153
|
+
/0x04/.test(t));
|
|
12154
|
+
});
|
|
12155
|
+
if (!hasPkLocalSig)
|
|
12156
|
+
return findings;
|
|
12157
|
+
const seen = new Set();
|
|
12158
|
+
for (let i = 0; i < lines.length; i++) {
|
|
12159
|
+
const trimmed = lines[i].trim();
|
|
12160
|
+
if (trimmed.startsWith('//'))
|
|
12161
|
+
continue;
|
|
12162
|
+
const failBranch = trimmed.match(/^if\s*\(\s*!(\w+)\s*\)\s*(.+)$/);
|
|
12163
|
+
if (!failBranch)
|
|
12164
|
+
continue;
|
|
12165
|
+
const tail = failBranch[2];
|
|
12166
|
+
if (/\bthrow\b/.test(tail))
|
|
12167
|
+
continue;
|
|
12168
|
+
if (!/\breturn\b/.test(tail))
|
|
12169
|
+
continue;
|
|
12170
|
+
if (!/\.(subarray|slice)\s*\(/.test(tail) &&
|
|
12171
|
+
!/Buffer\.from\s*\(/.test(tail)) {
|
|
12172
|
+
continue;
|
|
12173
|
+
}
|
|
12174
|
+
const lineNo = i + 1;
|
|
12175
|
+
if (seen.has(lineNo))
|
|
12176
|
+
continue;
|
|
12177
|
+
seen.add(lineNo);
|
|
12178
|
+
findings.push({
|
|
12179
|
+
id: `input_validation-${file}-${lineNo}-js-zip-header-failopen`,
|
|
12180
|
+
pass: 'language-sources',
|
|
12181
|
+
category: 'security',
|
|
12182
|
+
rule_id: 'input_validation',
|
|
12183
|
+
cwe: 'CWE-20',
|
|
12184
|
+
severity: 'high',
|
|
12185
|
+
level: 'error',
|
|
12186
|
+
message: 'Input validation fail-open: an invalid ZIP local header check still ' +
|
|
12187
|
+
'returns a slice of the caller-controlled buffer. On parse failure, ' +
|
|
12188
|
+
'throw or return empty — do not pass through untrusted bytes.',
|
|
12189
|
+
file,
|
|
12190
|
+
line: lineNo,
|
|
12191
|
+
snippet: trimmed.substring(0, 120),
|
|
12192
|
+
});
|
|
12193
|
+
}
|
|
11199
12194
|
return findings;
|
|
11200
12195
|
}
|
|
11201
12196
|
/**
|
|
@@ -11845,6 +12840,53 @@ export function findGoGobDeserializationFindings(code, file) {
|
|
|
11845
12840
|
}
|
|
11846
12841
|
return findings;
|
|
11847
12842
|
}
|
|
12843
|
+
/**
|
|
12844
|
+
* Blank the contents of JS/TS string and template literals and comments,
|
|
12845
|
+
* keeping newlines and every offset, so a line scanner does not match code
|
|
12846
|
+
* that only appears as text (a message describing `JSON.parse(req.body)`).
|
|
12847
|
+
*/
|
|
12848
|
+
function maskJsLiteralsAndComments(code) {
|
|
12849
|
+
const out = code.split('');
|
|
12850
|
+
let i = 0;
|
|
12851
|
+
const blank = (from, to) => {
|
|
12852
|
+
for (let k = from; k < to; k++)
|
|
12853
|
+
if (out[k] !== '\n')
|
|
12854
|
+
out[k] = ' ';
|
|
12855
|
+
};
|
|
12856
|
+
while (i < code.length) {
|
|
12857
|
+
const c = code[i];
|
|
12858
|
+
const n = code[i + 1];
|
|
12859
|
+
if (c === '/' && n === '/') {
|
|
12860
|
+
let j = i;
|
|
12861
|
+
while (j < code.length && code[j] !== '\n')
|
|
12862
|
+
j++;
|
|
12863
|
+
blank(i, j);
|
|
12864
|
+
i = j;
|
|
12865
|
+
}
|
|
12866
|
+
else if (c === '/' && n === '*') {
|
|
12867
|
+
const end = code.indexOf('*/', i + 2);
|
|
12868
|
+
const j = end < 0 ? code.length : end + 2;
|
|
12869
|
+
blank(i, j);
|
|
12870
|
+
i = j;
|
|
12871
|
+
}
|
|
12872
|
+
else if (c === '"' || c === "'" || c === '`') {
|
|
12873
|
+
let j = i + 1;
|
|
12874
|
+
while (j < code.length && code[j] !== c) {
|
|
12875
|
+
if (code[j] === '\\')
|
|
12876
|
+
j++;
|
|
12877
|
+
else if (c !== '`' && code[j] === '\n')
|
|
12878
|
+
break;
|
|
12879
|
+
j++;
|
|
12880
|
+
}
|
|
12881
|
+
blank(i + 1, Math.min(j, code.length));
|
|
12882
|
+
i = j + 1;
|
|
12883
|
+
}
|
|
12884
|
+
else {
|
|
12885
|
+
i++;
|
|
12886
|
+
}
|
|
12887
|
+
}
|
|
12888
|
+
return out.join('');
|
|
12889
|
+
}
|
|
11848
12890
|
/**
|
|
11849
12891
|
* Sprint 89 detector B (#189) — JS insecure deserialization via
|
|
11850
12892
|
* `JSON.parse(req.body)`.
|
|
@@ -11900,13 +12942,14 @@ export function findJsJsonParseBodyFindings(code, file) {
|
|
|
11900
12942
|
}
|
|
11901
12943
|
const callRe = /\bJSON\s*\.\s*parse\s*\(\s*([^)]+?)\s*\)/g;
|
|
11902
12944
|
const seen = new Set();
|
|
12945
|
+
// Calls only: `JSON.parse(req.body)` inside a string or comment is text.
|
|
12946
|
+
const codeLines = maskJsLiteralsAndComments(code).split('\n');
|
|
11903
12947
|
for (let i = 0; i < lines.length; i++) {
|
|
11904
12948
|
const line = lines[i];
|
|
11905
|
-
|
|
11906
|
-
continue;
|
|
12949
|
+
const codeLine = codeLines[i] ?? '';
|
|
11907
12950
|
callRe.lastIndex = 0;
|
|
11908
12951
|
let m;
|
|
11909
|
-
while ((m = callRe.exec(
|
|
12952
|
+
while ((m = callRe.exec(codeLine)) !== null) {
|
|
11910
12953
|
const arg = m[1];
|
|
11911
12954
|
let tainted = reqBodyRe.test(arg);
|
|
11912
12955
|
if (!tainted) {
|