From 04667c815e7fb25ba5d71cd8f10e5718e8e29712 Mon Sep 17 00:00:00 2001 From: Marius Ciepluch <11855163+norandom@users.noreply.github.com> Date: Mon, 12 Oct 2026 11:16:35 +0200 Subject: [PATCH] measure(g0): fair-target deep scan -- detection 3/5 on app-shaped+sound vs ~0 library-heavy Ran ousast scan --mode deep on the 5 app-shaped + sound pool targets (the fair change-scoped eval set from the shape classification). Detection 3/5: graphiti (hunter, Cypher injection), mcp-remote (hunter, url-scheme injection), dcmcp (ENGINE overlay -- 'outbound.request reached from parameter' on readFileFromUrl; parameter-as-source fired because an entry point is present). 2/5 missed: malla (DOM/template XSS) and dms (Jinja SSTI), both in families families.toml documents as limited. This validates the strategic pivot: the near-zero catch was substantially a TARGET MISMATCH -- on app-shaped repos the engine itself detects (0 -> 3/5). But all hits stop at suspicion/ static_corroboration; release-grade BLOCK still needs proof-arm elevation + family coverage + the DSPy hint loop. Cost ~$0.007/repo. So bet #1 (fix eval target) is necessary and material but not sufficient. Next: recompute a floor on an app-shaped sound subset / real app corpus (VAmPI, NodeGoat, DjanGoat, vibe-py); measure proof-arm suspicion->proven lift. Co-Authored-By: Claude Opus 4.8 --- .../engine-change-scoped-analysis/brief.md | 16 + .../2026-10-12-hunter-fair-targets/NOTES.md | 35 + .../dcmcp-findings.json | 490 +++++++ .../dms-findings.json | 416 ++++++ .../malla-findings.json | 1293 +++++++++++++++++ .../mcpremote-findings.json | 611 ++++++++ 6 files changed, 2861 insertions(+) create mode 100644 benchmarks/measurements/2026-10-12-hunter-fair-targets/NOTES.md create mode 100644 benchmarks/measurements/2026-10-12-hunter-fair-targets/dcmcp-findings.json create mode 100644 benchmarks/measurements/2026-10-12-hunter-fair-targets/dms-findings.json create mode 100644 benchmarks/measurements/2026-10-12-hunter-fair-targets/malla-findings.json create mode 100644 benchmarks/measurements/2026-10-12-hunter-fair-targets/mcpremote-findings.json diff --git a/.kiro/specs/engine-change-scoped-analysis/brief.md b/.kiro/specs/engine-change-scoped-analysis/brief.md index 11905c6..f69dd76 100644 --- a/.kiro/specs/engine-change-scoped-analysis/brief.md +++ b/.kiro/specs/engine-change-scoped-analysis/brief.md @@ -417,3 +417,19 @@ scored against a ~90%-illegitimate denominator — the near-zero catch is largel **Next:** deep scan (regex/engine/hunter) on the 5 fair targets → per-tier catch rate on the corrected set; then stand up an app-shaped eval set from known-vulnerable apps (VAmPI/NodeGoat/DjanGoat/vibe-py) and recompute the floor. Deferred bets (advisory-with-conditions, reachability prover) revisited once the fair-target numbers land. + +## FAIR-TARGET RESULT: detection 3/5 on app-shaped+sound (vs ~0 library-heavy) — target-fix validated (2026-10-12) + +Deep scan on the 5 app-shaped+sound targets (`benchmarks/measurements/2026-10-12-hunter-fair-targets/`): +- **3/5 detected**: graphiti (hunter, Cypher injection), mcp-remote (hunter, url-scheme injection), dcmcp (ENGINE + overlay — "outbound.request reached from parameter" on readFileFromUrl; the param-as-source mode fired because an + entry point is present). **2/5 missed**: malla (DOM/template XSS) and dms (Jinja SSTI) — both in families + families.toml DOCUMENTS as limited (template-mediated escaping). +- vs the library-heavy pilot's ~0 → **the target mismatch was real and material.** On an app-shaped repo the engine + itself detects (parameter is a recognized source once the entry point is in-repo). +- **But everything stops at `suspicion`/`static_corroboration`** — none proven. Cost ~$0.007/repo. + +**Strategy verdict:** bet #1 (fix the eval target) is validated as NECESSARY and MATERIAL but NOT SUFFICIENT. +Detection appears on fair targets; release-grade BLOCK still needs (a) proof-arm elevation (suspicion→proven) and +(b) family coverage (template XSS/SSTI) + the DSPy hint loop for misses. Next: recompute a floor on an app-shaped +sound subset / real app corpus (VAmPI, NodeGoat, DjanGoat, vibe-py), and measure the proof arm's suspicion→proven lift. diff --git a/benchmarks/measurements/2026-10-12-hunter-fair-targets/NOTES.md b/benchmarks/measurements/2026-10-12-hunter-fair-targets/NOTES.md new file mode 100644 index 0000000..bd659b8 --- /dev/null +++ b/benchmarks/measurements/2026-10-12-hunter-fair-targets/NOTES.md @@ -0,0 +1,35 @@ +# Deep scan (regex/engine/hunter) on the 5 app-shaped + sound "fair" targets — 2026-10-12 + +## Why +Strategic pivot "fix the evaluation target first". The shape classification left 5 pool targets that are BOTH +label-sound AND application-shaped (reachability in-repo) — the fair change-scoped eval set. Ran `ousast scan +--mode deep` (DeepSeek, model layer + tool_hunter, 60-call bound) on all 5 to get a per-tier catch rate on the +corrected target set (vs ~0 on the library-heavy pilot). graphiti from the earlier run; the other 4 here. + +## Result: detection 3/5 on fair targets (vs ~0 library-heavy), all at suspicion rung +| target | family | detected | by | rung | model calls / cost | +|---|---|---|---|---|---| +| getzep/graphiti | injection (Cypher) | YES | hunter (family:cypher-injection, build_fulltext_query L278) | suspicion | 64 / $0.0071 | +| punkpeye/mcp-remote | injection (url-scheme) | YES | hunter (family:url-scheme-injection, sanitizeUrl L708) | suspicion | 65 / $0.0075 | +| sorlen008/desktopcommandermcp | untrusted_destination (SSRF) | YES | engine overlay ("outbound.request reached from parameter", readFileFromUrl L174) | suspicion | 60 / $0.0067 | +| zenitram/malla | output_encoding (DOM XSS) | no | — (no finding on the vuln JS file) | — | 65 / $0.0072 | +| adfinis/document-merge-service | injection (Jinja SSTI) | no | — (no finding on jinja.py get_jinja_env) | — | 64 / $0.0075 | + +## Reads +1. **The target mismatch was real and material: detection 0 → 3/5 when the target is app-shaped + sound.** On + dcmcp (an MCP server) the ENGINE's parameter-as-source mode fired (entry point present) and flagged the SSRF — + the exact thing it could NOT do on library code. On graphiti/mcpremote the hunter caught injection with a + family tag. The near-zero pilot catch was substantially a target artifact. +2. **The 2 misses are in families the tool DOCUMENTS as limited** — malla is DOM/template XSS (output_encoding's + `limits` in families.toml: cannot decide template-mediated escaping) and dms is Jinja SSTI/autoescape-config. + Expected coverage gaps, not reachability failures. +3. **Everything still stops at `suspicion`/`static_corroboration`** — none reached corroborated/entailed/ + execution_confirmed. Release-grade BLOCK needs the proof arm to elevate these. +4. Cost remains a non-issue: ~$0.007/repo, ~60-65 model calls for a whole-repo deep scan. + +## Conclusion for the strategy +Fixing the evaluation target (bet #1) is validated as necessary and material but NOT sufficient: on fair targets +detection appears (3/5) but stays at suspicion. The remaining work is (a) proof-arm elevation (suspicion→proven) +for BLOCK, and (b) family coverage for the documented-limited families — and the DSPy hint loop for the misses. +This is also the honest basis to recompute record-07 on a sound+app-shaped subset / a real app corpus (VAmPI, +NodeGoat, DjanGoat, vibe-py). diff --git a/benchmarks/measurements/2026-10-12-hunter-fair-targets/dcmcp-findings.json b/benchmarks/measurements/2026-10-12-hunter-fair-targets/dcmcp-findings.json new file mode 100644 index 0000000..40a1d2e --- /dev/null +++ b/benchmarks/measurements/2026-10-12-hunter-fair-targets/dcmcp-findings.json @@ -0,0 +1,490 @@ +{ + "findings": [ + { + "confidence": "medium", + "evidence_level": "static_corroboration", + "finding_id": "js-command-exec:setup-claude-server.js:36", + "function_name": null, + "line": 36, + "path": "setup-claude-server.js", + "proposed_snippet": null, + "ranking_priority": 1.5, + "rationale": "Static pattern js-command-exec (CWE-78) matched exec on line 36; manual verification still required.", + "reachability_conditions": [], + "reachability_evidence": [], + "reachability_status": "unknown", + "rung": "suspicion", + "severity": "critical", + "status": "enabled", + "tags": [ + "auth_boundary", + "deserialization", + "filesystem_entry", + "network_entry", + "parser", + "syscall_entry" + ], + "title": "Node child_process command execution needs review" + }, + { + "confidence": "medium", + "evidence_level": "static_corroboration", + "finding_id": "js-command-exec:setup-claude-server.js:216", + "function_name": null, + "line": 216, + "path": "setup-claude-server.js", + "proposed_snippet": null, + "ranking_priority": 1.5, + "rationale": "Static pattern js-command-exec (CWE-78) matched exec on line 216; manual verification still required.", + "reachability_conditions": [], + "reachability_evidence": [], + "reachability_status": "unknown", + "rung": "suspicion", + "severity": "critical", + "status": "enabled", + "tags": [ + "auth_boundary", + "deserialization", + "filesystem_entry", + "network_entry", + "parser", + "syscall_entry" + ], + "title": "Node child_process command execution needs review" + }, + { + "confidence": "medium", + "evidence_level": "static_corroboration", + "finding_id": "overlay-coverage:src/terminal-manager.ts:57:prototype.merge", + "function_name": null, + "line": 57, + "path": "src/terminal-manager.ts", + "proposed_snippet": null, + "ranking_priority": 0.0, + "rationale": "uninventoried sink prototype.merge reached from parameter", + "reachability_conditions": [], + "reachability_evidence": [], + "reachability_status": "unknown", + "rung": "suspicion", + "severity": "info", + "status": "enabled", + "tags": [ + "overlay-originated" + ], + "title": "Overlay coverage: prototype.merge" + }, + { + "confidence": "medium", + "evidence_level": "static_corroboration", + "finding_id": "overlay-coverage:src/terminal-manager.ts:84:prototype.merge", + "function_name": null, + "line": 84, + "path": "src/terminal-manager.ts", + "proposed_snippet": null, + "ranking_priority": 0.0, + "rationale": "uninventoried sink prototype.merge reached from parameter", + "reachability_conditions": [], + "reachability_evidence": [], + "reachability_status": "unknown", + "rung": "suspicion", + "severity": "info", + "status": "enabled", + "tags": [ + "overlay-originated" + ], + "title": "Overlay coverage: prototype.merge" + }, + { + "confidence": "medium", + "evidence_level": "static_corroboration", + "finding_id": "overlay-coverage:src/tools/edit.ts:11:path.open", + "function_name": null, + "line": 11, + "path": "src/tools/edit.ts", + "proposed_snippet": null, + "ranking_priority": 0.0, + "rationale": "uninventoried sink path.open reached from parameter", + "reachability_conditions": [], + "reachability_evidence": [], + "reachability_status": "unknown", + "rung": "suspicion", + "severity": "medium", + "status": "enabled", + "tags": [ + "overlay-originated" + ], + "title": "Overlay coverage: path.open" + }, + { + "confidence": "medium", + "evidence_level": "static_corroboration", + "finding_id": "overlay-coverage:src/tools/filesystem.ts:174:outbound.request", + "function_name": null, + "line": 174, + "path": "src/tools/filesystem.ts", + "proposed_snippet": null, + "ranking_priority": 0.0, + "rationale": "uninventoried sink outbound.request reached from parameter", + "reachability_conditions": [], + "reachability_evidence": [], + "reachability_status": "unknown", + "rung": "suspicion", + "severity": "medium", + "status": "enabled", + "tags": [ + "overlay-originated" + ], + "title": "Overlay coverage: outbound.request" + }, + { + "confidence": "medium", + "evidence_level": "static_corroboration", + "finding_id": "overlay-coverage:src/tools/filesystem.ts:321:path.open", + "function_name": null, + "line": 321, + "path": "src/tools/filesystem.ts", + "proposed_snippet": null, + "ranking_priority": 0.0, + "rationale": "uninventoried sink path.open reached from parameter", + "reachability_conditions": [], + "reachability_evidence": [], + "reachability_status": "unknown", + "rung": "suspicion", + "severity": "medium", + "status": "enabled", + "tags": [ + "overlay-originated" + ], + "title": "Overlay coverage: path.open" + }, + { + "confidence": "medium", + "evidence_level": "static_corroboration", + "finding_id": "overlay-coverage:src/tools/search.ts:176:path.open", + "function_name": null, + "line": 176, + "path": "src/tools/search.ts", + "proposed_snippet": null, + "ranking_priority": 0.0, + "rationale": "uninventoried sink path.open reached from parameter", + "reachability_conditions": [], + "reachability_evidence": [], + "reachability_status": "unknown", + "rung": "suspicion", + "severity": "medium", + "status": "enabled", + "tags": [ + "overlay-originated" + ], + "title": "Overlay coverage: path.open" + }, + { + "confidence": "low", + "evidence_level": "suspicion", + "finding_id": "tool-hunter:scripts/sync-version.js:24", + "function_name": null, + "line": 24, + "path": "scripts/sync-version.js", + "proposed_snippet": null, + "ranking_priority": 15.1974, + "rationale": "sync-version.js reads package.json, optionally bumps the version, and writes src/version.ts via template literal `export const VERSION = '${version}';`. The version value is taken from package.json without validation; a crafted version string containing a quote could break out of the string literal and inject code into the generated TypeScript source. This is the deserialization/parser/filesystem_entry hotspot.", + "reachability_conditions": [], + "reachability_evidence": [], + "reachability_status": "unknown", + "rung": "suspicion", + "severity": "medium", + "status": "enabled", + "tags": [ + "tool-hunter", + "family:code-injection" + ], + "title": "Version string interpolated into generated source file without escaping" + }, + { + "confidence": "low", + "evidence_level": "suspicion", + "finding_id": "tool-hunter:setup-claude-server.js:14", + "function_name": null, + "line": 14, + "path": "setup-claude-server.js", + "proposed_snippet": null, + "ranking_priority": 22.5805, + "rationale": "The setup script embeds a literal PostHog project API key and imports node-machine-id to derive a stable uniqueUserId, then sends analytics with flushAt:1. This is a hardcoded credential plus device fingerprinting in an install/setup entrypoint (network_entry, syscall_entry tags). The key is committed to the repository and cannot be rotated without a code change.", + "reachability_conditions": [], + "reachability_evidence": [], + "reachability_status": "unknown", + "rung": "suspicion", + "severity": "medium", + "status": "enabled", + "tags": [ + "tool-hunter", + "family:hardcoded-secret" + ], + "title": "Hardcoded PostHog API key and machine-id telemetry on startup" + }, + { + "confidence": "low", + "evidence_level": "suspicion", + "finding_id": "tool-hunter:setup-claude-server.js:38", + "function_name": null, + "line": 38, + "path": "setup-claude-server.js", + "proposed_snippet": null, + "ranking_priority": 22.5805, + "rationale": "getNpmVersion invokes exec('npm --version'), which runs through a shell. The command is static so not directly injectable, but it establishes a shell-execution syscall entry in the setup script that also handles filesystem writes and network telemetry, matching the syscall_entry/network_entry tags on this top-ranked hotspot.", + "reachability_conditions": [], + "reachability_evidence": [], + "reachability_status": "unknown", + "rung": "suspicion", + "severity": "medium", + "status": "enabled", + "tags": [ + "tool-hunter", + "family:command-execution" + ], + "title": "exec of npm --version in setup entrypoint" + }, + { + "confidence": "low", + "evidence_level": "suspicion", + "finding_id": "tool-hunter:src/command-manager.ts:152", + "function_name": null, + "line": 152, + "path": "src/command-manager.ts", + "proposed_snippet": null, + "ranking_priority": 0.0, + "rationale": "validateCommand relies on extractCommands/getBaseCommand, which split on separators and take the first whitespace-delimited token lowercased. Blocklist matching is exact-string against blockedCommands. A blocked command invoked as '/bin/rm', './rm', 'rm.exe', or via env/quoting (e.g. 'r''m', '\\rm', 'command rm') yields a base command that does not equal the blocked entry, so the blocklist is trivially evaded. This is the auth_boundary/parser hotspot flagged in src/tools/execute.ts and src/config-manager.ts.", + "reachability_conditions": [], + "reachability_evidence": [], + "reachability_status": "unknown", + "rung": "suspicion", + "severity": "medium", + "status": "enabled", + "tags": [ + "tool-hunter", + "family:command-injection" + ], + "title": "Command blocklist validation is bypassable via shell metacharacters and path-qualified binaries" + }, + { + "confidence": "low", + "evidence_level": "suspicion", + "finding_id": "tool-hunter:src/config-manager.ts:44", + "function_name": null, + "line": 44, + "path": "src/config-manager.ts", + "proposed_snippet": null, + "ranking_priority": 19.6849, + "rationale": "init() reads ~/.claude-server-commander/config.json and assigns JSON.parse(configData) directly to this.config with no schema or type validation. A malformed or attacker-controlled config file can set allowedDirectories to '/' or blockedCommands to [], or inject unexpected keys (the interface has an index signature [key: string]: any). This is the deserialization/filesystem_entry/auth_boundary hotspot.", + "reachability_conditions": [], + "reachability_evidence": [], + "reachability_status": "unknown", + "rung": "suspicion", + "severity": "medium", + "status": "enabled", + "tags": [ + "tool-hunter", + "family:unsafe-deserialization" + ], + "title": "Config JSON parsed from disk without schema validation" + }, + { + "confidence": "low", + "evidence_level": "suspicion", + "finding_id": "tool-hunter:src/config-manager.ts:76", + "function_name": null, + "line": 76, + "path": "src/config-manager.ts", + "proposed_snippet": null, + "ranking_priority": 19.6849, + "rationale": "getDefaultConfig ships a fixed denylist of dangerous commands (mkfs, dd, sudo, etc.). Denylists are inherently incomplete: commands not enumerated (e.g. 'sh', 'bash', 'python', 'curl', 'nc', 'chmod', 'kill') are permitted by default. Combined with the base-command matching weakness in command-manager.ts, the default posture is permissive. This is the auth_boundary hotspot in config-manager.ts.", + "reachability_conditions": [], + "reachability_evidence": [], + "reachability_status": "unknown", + "rung": "suspicion", + "severity": "medium", + "status": "enabled", + "tags": [ + "tool-hunter", + "family:insecure-default" + ], + "title": "Default blockedCommands is a denylist, not an allowlist" + }, + { + "confidence": "low", + "evidence_level": "suspicion", + "finding_id": "tool-hunter:src/handlers/filesystem-handlers.ts:51", + "function_name": null, + "line": 51, + "path": "src/handlers/filesystem-handlers.ts", + "proposed_snippet": null, + "ranking_priority": 14.1621, + "rationale": "handleReadFile calls readFile(parsed.path, parsed.isUrl). When isUrl is true, filesystem.ts routes to readFileFromUrl which performs fetch(url). The URL is not validated against any allowlist or private-IP blocklist, so the handler can be used to fetch internal/loopback/metadata endpoints (SSRF). This is the filesystem_entry/parser hotspot in filesystem-handlers.ts.", + "reachability_conditions": [], + "reachability_evidence": [], + "reachability_status": "unknown", + "rung": "suspicion", + "severity": "medium", + "status": "enabled", + "tags": [ + "tool-hunter", + "family:ssrf" + ], + "title": "read_file handler forwards isUrl flag to readFile, enabling server-side request forgery" + }, + { + "confidence": "low", + "evidence_level": "suspicion", + "finding_id": "tool-hunter:src/tools/config.ts:65", + "function_name": null, + "line": 65, + "path": "src/tools/config.ts", + "proposed_snippet": null, + "ranking_priority": 16.2332, + "rationale": "setConfigValue accepts arbitrary keys including allowedDirectories and blockedCommands and writes them via configManager.setValue. A caller can widen allowedDirectories to '/' or empty the blockedCommands array, disabling both the filesystem and command boundaries at runtime. The server.ts description itself warns this must be used in a separate chat, confirming it is a security-sensitive control surface reachable through the same tool interface.", + "reachability_conditions": [], + "reachability_evidence": [], + "reachability_status": "unknown", + "rung": "suspicion", + "severity": "medium", + "status": "enabled", + "tags": [ + "tool-hunter", + "family:privilege-escalation" + ], + "title": "set_config_value allows runtime mutation of allowedDirectories and blockedCommands" + }, + { + "confidence": "low", + "evidence_level": "suspicion", + "finding_id": "tool-hunter:src/tools/execute.ts:32", + "function_name": null, + "line": 32, + "path": "src/tools/execute.ts", + "proposed_snippet": null, + "ranking_priority": 13.5639, + "rationale": "executeCommand extracts commands for analytics, then calls commandManager.validateCommand, then terminalManager.executeCommand. The only authorization check is the blocklist-based validateCommand, which is bypassable (see command-manager finding). There is no allowlist, no shell-escape sanitization, and the raw parsed.data.command string is passed to the terminal manager. This is the auth_boundary hotspot in execute.ts.", + "reachability_conditions": [], + "reachability_evidence": [], + "reachability_status": "unknown", + "rung": "suspicion", + "severity": "medium", + "status": "enabled", + "tags": [ + "tool-hunter", + "family:command-injection" + ], + "title": "Command validation occurs after analytics extraction and is the sole gate before execution" + }, + { + "confidence": "low", + "evidence_level": "suspicion", + "finding_id": "tool-hunter:src/tools/filesystem.ts:101", + "function_name": null, + "line": 101, + "path": "src/tools/filesystem.ts", + "proposed_snippet": null, + "ranking_priority": 0.0, + "rationale": "When the requested path does not exist and validateParentDirectories returns false, validatePath still returns the absolute path rather than throwing. Callers (readFile, writeFile, search, move) treat the return as validated, so a non-existent path outside allowed directories can be passed through to subsequent operations. The allowlist check happens before realpath resolution, so symlink-based escapes are also possible for existing paths.", + "reachability_conditions": [], + "reachability_evidence": [], + "reachability_status": "unknown", + "rung": "suspicion", + "severity": "medium", + "status": "enabled", + "tags": [ + "tool-hunter", + "family:path-traversal" + ], + "title": "validatePath returns unvalidated absolute path when no valid parent exists" + }, + { + "confidence": "low", + "evidence_level": "suspicion", + "finding_id": "tool-hunter:src/tools/filesystem.ts:78", + "function_name": null, + "line": 78, + "path": "src/tools/filesystem.ts", + "proposed_snippet": null, + "ranking_priority": 0.0, + "rationale": "isPathAllowed returns true if allowedDirectories includes '/' OR allowedDirectories.length === 0. getAllowedDirs returns [] on any error (catch block), and validatePath's error path also returns the absolute path anyway. Combined with config-manager defaulting allowedDirectories to homedir, an empty/failed config silently grants access to all paths, defeating the filesystem boundary tagged on src/tools/search.ts, src/handlers/filesystem-handlers.ts, and src/config-manager.ts.", + "reachability_conditions": [], + "reachability_evidence": [], + "reachability_status": "unknown", + "rung": "suspicion", + "severity": "medium", + "status": "enabled", + "tags": [ + "tool-hunter", + "family:path-traversal" + ], + "title": "Path allowlist fails open when allowedDirectories is empty or '/'" + }, + { + "confidence": "low", + "evidence_level": "suspicion", + "finding_id": "tool-hunter:src/tools/search.ts:36", + "function_name": null, + "line": 36, + "path": "src/tools/search.ts", + "proposed_snippet": null, + "ranking_priority": 19.995, + "rationale": "The pattern and filePattern are pushed into the ripgrep argv array. While spawn avoids shell interpretation, a pattern beginning with '-' can be interpreted as a ripgrep flag (argument injection), and filePattern via -g can broaden or redirect the search scope. validatePath is applied to rootPath but not to the pattern/filePattern, so the deserialization/parser hotspot can be steered outside intended semantics.", + "reachability_conditions": [], + "reachability_evidence": [], + "reachability_status": "unknown", + "rung": "suspicion", + "severity": "medium", + "status": "enabled", + "tags": [ + "tool-hunter", + "family:argument-injection" + ], + "title": "searchCode passes user pattern directly to ripgrep spawn without argument hardening" + }, + { + "confidence": "low", + "evidence_level": "suspicion", + "finding_id": "tool-hunter:src/tools/search.ts:88", + "function_name": null, + "line": 88, + "path": "src/tools/search.ts", + "proposed_snippet": null, + "ranking_priority": 19.995, + "rationale": "stdoutBuffer accumulates all ripgrep output in memory before splitting and JSON.parse-ing each line. A large or adversarial search result set can exhaust memory, and the per-line JSON.parse is wrapped in a catch that only logs, so malformed output is silently dropped. This is the deserialization/parser tag on this high-band hotspot.", + "reachability_conditions": [], + "reachability_evidence": [], + "reachability_status": "unknown", + "rung": "suspicion", + "severity": "medium", + "status": "enabled", + "tags": [ + "tool-hunter", + "family:resource-exhaustion" + ], + "title": "Unbounded stdout buffering and JSON.parse of ripgrep output" + }, + { + "confidence": "low", + "evidence_level": "suspicion", + "finding_id": "tool-hunter:test/test-blocked-commands.js:147", + "function_name": null, + "line": 147, + "path": "test/test-blocked-commands.js", + "proposed_snippet": null, + "ranking_priority": 13.5864, + "rationale": "The test sets blockedCommands to a custom list, asserts behavior, then teardown calls configManager.updateConfig(originalConfig). Because the test operates on the real config file (~/.claude-server-commander/config.json) rather than an isolated temp config, a crash between setValue and teardown leaves the persisted blocklist modified, weakening the command boundary. This is the filesystem_entry/parser hotspot in the test file.", + "reachability_conditions": [], + "reachability_evidence": [], + "reachability_status": "unknown", + "rung": "suspicion", + "severity": "medium", + "status": "enabled", + "tags": [ + "tool-hunter", + "family:test-isolation" + ], + "title": "Test mutates live blockedCommands config and restores via updateConfig" + } + ] +} diff --git a/benchmarks/measurements/2026-10-12-hunter-fair-targets/dms-findings.json b/benchmarks/measurements/2026-10-12-hunter-fair-targets/dms-findings.json new file mode 100644 index 0000000..c482d86 --- /dev/null +++ b/benchmarks/measurements/2026-10-12-hunter-fair-targets/dms-findings.json @@ -0,0 +1,416 @@ +{ + "findings": [ + { + "confidence": "medium", + "evidence_level": "static_corroboration", + "finding_id": "overlay-coverage:document_merge_service/api/authentication.py:67:outbound.request", + "function_name": null, + "line": 67, + "path": "document_merge_service/api/authentication.py", + "proposed_snippet": null, + "ranking_priority": 0.0, + "rationale": "uninventoried sink outbound.request reached from parameter", + "reachability_conditions": [], + "reachability_evidence": [], + "reachability_status": "unknown", + "rung": "suspicion", + "severity": "medium", + "status": "enabled", + "tags": [ + "overlay-originated" + ], + "title": "Overlay coverage: outbound.request" + }, + { + "confidence": "medium", + "evidence_level": "static_corroboration", + "finding_id": "overlay-coverage:document_merge_service/api/tests/data/__init__.py:14:path.open", + "function_name": null, + "line": 14, + "path": "document_merge_service/api/tests/data/__init__.py", + "proposed_snippet": null, + "ranking_priority": 0.0, + "rationale": "uninventoried sink path.open reached from parameter", + "reachability_conditions": [], + "reachability_evidence": [], + "reachability_status": "unknown", + "rung": "suspicion", + "severity": "medium", + "status": "enabled", + "tags": [ + "overlay-originated" + ], + "title": "Overlay coverage: path.open" + }, + { + "confidence": "medium", + "evidence_level": "static_corroboration", + "finding_id": "overlay-coverage:document_merge_service/api/views.py:39:html.unescaped", + "function_name": null, + "line": 39, + "path": "document_merge_service/api/views.py", + "proposed_snippet": null, + "ranking_priority": 0.0, + "rationale": "uninventoried sink html.unescaped reached from request", + "reachability_conditions": [], + "reachability_evidence": [], + "reachability_status": "unknown", + "rung": "suspicion", + "severity": "medium", + "status": "enabled", + "tags": [ + "overlay-originated" + ], + "title": "Overlay coverage: html.unescaped" + }, + { + "confidence": "medium", + "evidence_level": "static_corroboration", + "finding_id": "overlay-coverage:document_merge_service/api/views.py:52:outbound.request", + "function_name": null, + "line": 52, + "path": "document_merge_service/api/views.py", + "proposed_snippet": null, + "ranking_priority": 0.0, + "rationale": "uninventoried sink outbound.request reached from request", + "reachability_conditions": [], + "reachability_evidence": [], + "reachability_status": "unknown", + "rung": "suspicion", + "severity": "medium", + "status": "enabled", + "tags": [ + "overlay-originated" + ], + "title": "Overlay coverage: outbound.request" + }, + { + "confidence": "medium", + "evidence_level": "static_corroboration", + "finding_id": "overlay-coverage:document_merge_service/api/views.py:57:html.unescaped", + "function_name": null, + "line": 57, + "path": "document_merge_service/api/views.py", + "proposed_snippet": null, + "ranking_priority": 0.0, + "rationale": "uninventoried sink html.unescaped reached from request", + "reachability_conditions": [], + "reachability_evidence": [], + "reachability_status": "unknown", + "rung": "suspicion", + "severity": "medium", + "status": "enabled", + "tags": [ + "overlay-originated" + ], + "title": "Overlay coverage: html.unescaped" + }, + { + "confidence": "medium", + "evidence_level": "static_corroboration", + "finding_id": "overlay-coverage:document_merge_service/api/views.py:79:html.unescaped", + "function_name": null, + "line": 79, + "path": "document_merge_service/api/views.py", + "proposed_snippet": null, + "ranking_priority": 0.0, + "rationale": "uninventoried sink html.unescaped reached from parameter", + "reachability_conditions": [], + "reachability_evidence": [], + "reachability_status": "unknown", + "rung": "suspicion", + "severity": "medium", + "status": "enabled", + "tags": [ + "overlay-originated" + ], + "title": "Overlay coverage: html.unescaped" + }, + { + "confidence": "medium", + "evidence_level": "suspicion", + "finding_id": "model:untrusted_destination:document_merge_service/api/views.py:52:merge", + "function_name": "merge", + "line": 52, + "path": "document_merge_service/api/views.py", + "proposed_snippet": null, + "ranking_priority": 0.0, + "rationale": "request.data -> requests.post(url, files = {\"file\":response.content}) in document_merge_service/api/views.py (line 52, 17 steps)", + "reachability_conditions": [], + "reachability_evidence": [], + "reachability_status": "unknown", + "rung": "model_entailed", + "severity": "medium", + "status": "enabled", + "tags": [ + "family:untrusted_destination", + "rung:model_entailed" + ], + "title": "untrusted_destination (model_entailed)" + }, + { + "confidence": "low", + "evidence_level": "suspicion", + "finding_id": "tool-hunter:document_merge_service/api/authentication.py:32", + "function_name": null, + "line": 32, + "path": "document_merge_service/api/authentication.py", + "proposed_snippet": null, + "ranking_priority": 11.7422, + "rationale": "self.groups = userinfo.get(settings.OIDC_GROUPS_CLAIM) or []. Groups drive access control in views.py:25 (queryset.filter(group__in=self.request.user.groups)) and serializers.py:25. The claim is trusted verbatim from the userinfo response with no validation of issuer/audience, so any party able to influence that response controls group membership.", + "reachability_conditions": [], + "reachability_evidence": [], + "reachability_status": "unknown", + "rung": "suspicion", + "severity": "medium", + "status": "enabled", + "tags": [ + "tool-hunter", + "family:authorization" + ], + "title": "Authorization groups taken directly from OIDC userinfo claim" + }, + { + "confidence": "low", + "evidence_level": "suspicion", + "finding_id": "tool-hunter:document_merge_service/api/authentication.py:68", + "function_name": null, + "line": 68, + "path": "document_merge_service/api/authentication.py", + "proposed_snippet": null, + "ranking_priority": 11.7422, + "rationale": "requests.get(settings.OIDC_USERINFO_ENDPOINT, verify=settings.OIDC_VERIFY_SSL, ...). OIDC_VERIFY_SSL is env-controlled (settings.py:193, default True). If set false, the bearer-token identity lookup accepts any TLS cert, allowing a MITM to forge userinfo (sub/groups) and thus impersonate users and gain group-based access.", + "reachability_conditions": [], + "reachability_evidence": [], + "reachability_status": "unknown", + "rung": "suspicion", + "severity": "medium", + "status": "enabled", + "tags": [ + "tool-hunter", + "family:transport-security" + ], + "title": "OIDC userinfo fetched with configurable SSL verification" + }, + { + "confidence": "low", + "evidence_level": "suspicion", + "finding_id": "tool-hunter:document_merge_service/api/authentication.py:88", + "function_name": null, + "line": 88, + "path": "document_merge_service/api/authentication.py", + "proposed_snippet": null, + "ranking_priority": 11.7422, + "rationale": "cache.get_or_set(f\"authentication.userinfo.{hashsum_token}\", userinfo_method, timeout=settings.OIDC_BEARER_TOKEN_REVALIDATION_TIME). OIDC_BEARER_TOKEN_REVALIDATION_TIME defaults to 0 (settings.py:197-198). A 0 timeout means the userinfo is not cached, forcing a network call per request; conversely a large value keeps revoked tokens valid. The cache key is a sha256 of the token, so cache contents are keyed by token hash.", + "reachability_conditions": [], + "reachability_evidence": [], + "reachability_status": "unknown", + "rung": "suspicion", + "severity": "medium", + "status": "enabled", + "tags": [ + "tool-hunter", + "family:session-management" + ], + "title": "Bearer token cached by hash with configurable revalidation timeout" + }, + { + "confidence": "low", + "evidence_level": "suspicion", + "finding_id": "tool-hunter:document_merge_service/api/engines.py:52", + "function_name": null, + "line": 52, + "path": "document_merge_service/api/engines.py", + "proposed_snippet": null, + "ranking_priority": 10.7693, + "rationale": "return ENGINES[engine](template) indexes the ENGINES dict with the engine value. The engine comes from the template model/serializer (serializers.py:31-34, views.py:36). A KeyError on an unknown engine would surface as a 500 rather than a validation error; the value is constrained by model choices but the serializer path passes data.get(\"engine\") before full validation.", + "reachability_conditions": [], + "reachability_evidence": [], + "reachability_status": "unknown", + "rung": "suspicion", + "severity": "medium", + "status": "enabled", + "tags": [ + "tool-hunter", + "family:input-validation" + ], + "title": "Engine dispatch via dict lookup on user-controlled engine value" + }, + { + "confidence": "low", + "evidence_level": "suspicion", + "finding_id": "tool-hunter:document_merge_service/api/migrations/0001_initial.py:22", + "function_name": null, + "line": 22, + "path": "document_merge_service/api/migrations/0001_initial.py", + "proposed_snippet": null, + "ranking_priority": 9.4474, + "rationale": "(\"template\", models.FileField(max_length=1024, upload_to=\"\")) stores uploaded files directly in the media root with no subdirectory. Combined with the download view (views.py:66-84) which serves template.template.read() with a Content-Disposition attachment, uploaded filenames are not namespaced, raising collision/overwrite concerns.", + "reachability_conditions": [], + "reachability_evidence": [], + "reachability_status": "unknown", + "rung": "suspicion", + "severity": "medium", + "status": "enabled", + "tags": [ + "tool-hunter", + "family:file-handling" + ], + "title": "Template FileField with upload_to empty string" + }, + { + "confidence": "low", + "evidence_level": "suspicion", + "finding_id": "tool-hunter:document_merge_service/api/serializers.py:25", + "function_name": null, + "line": 25, + "path": "document_merge_service/api/serializers.py", + "proposed_snippet": null, + "ranking_priority": 0.0, + "rationale": "if group and group not in request.user.groups: raise ValidationError. This is the write-side counterpart to the read-side filter in views.py:25. It relies on request.user.groups being trustworthy, which traces back to the unvalidated OIDC claim in authentication.py:32.", + "reachability_conditions": [], + "reachability_evidence": [], + "reachability_status": "unknown", + "rung": "suspicion", + "severity": "medium", + "status": "enabled", + "tags": [ + "tool-hunter", + "family:authorization" + ], + "title": "Group assignment validated against request user's groups" + }, + { + "confidence": "low", + "evidence_level": "suspicion", + "finding_id": "tool-hunter:document_merge_service/api/tests/test_authentication.py:25", + "function_name": null, + "line": 25, + "path": "document_merge_service/api/tests/test_authentication.py", + "proposed_snippet": null, + "ranking_priority": 11.908, + "rationale": "userinfo = {\"sub\": \"1\", settings.OIDC_GROUPS_CLAIM: [\"test\"]} and the test asserts user.group == \"test\". The test confirms the implementation trusts the userinfo claim for group membership; no test covers issuer/audience verification or rejection of a mismatched token, so the trust boundary is untested.", + "reachability_conditions": [], + "reachability_evidence": [], + "reachability_status": "unknown", + "rung": "suspicion", + "severity": "medium", + "status": "enabled", + "tags": [ + "tool-hunter", + "family:test-coverage" + ], + "title": "Auth test asserts group from mocked userinfo without issuer validation" + }, + { + "confidence": "low", + "evidence_level": "suspicion", + "finding_id": "tool-hunter:document_merge_service/api/views.py:25", + "function_name": null, + "line": 25, + "path": "document_merge_service/api/views.py", + "proposed_snippet": null, + "ranking_priority": 0.0, + "rationale": "queryset.filter(group__in=self.request.user.groups) only applies when settings.GROUP_ACCESS_ONLY is true (default False, settings.py:190). When disabled, all templates are listed regardless of group. When enabled, AnonymousUser.groups is [] so filtering yields nothing, but the security posture hinges entirely on this env flag.", + "reachability_conditions": [], + "reachability_evidence": [], + "reachability_status": "unknown", + "rung": "suspicion", + "severity": "medium", + "status": "enabled", + "tags": [ + "tool-hunter", + "family:authorization" + ], + "title": "Group-based queryset filtering depends on user.groups being populated" + }, + { + "confidence": "low", + "evidence_level": "suspicion", + "finding_id": "tool-hunter:document_merge_service/api/views.py:47", + "function_name": null, + "line": 47, + "path": "document_merge_service/api/views.py", + "proposed_snippet": null, + "ranking_priority": 0.0, + "rationale": "requests.post(f\"{settings.UNOCONV_URL}/unoconv/{convert}\", files={\"file\": response.content}) sends the rendered document to an external conversion service. The convert value is validated against UNOCONV_ALLOWED_TYPES (serializers.py:52-58) and UNOCONV_URL must be configured, but the response content/status are passed straight back to the client (views.py:52-56), so the external service's output is trusted.", + "reachability_conditions": [], + "reachability_evidence": [], + "reachability_status": "unknown", + "rung": "suspicion", + "severity": "medium", + "status": "enabled", + "tags": [ + "tool-hunter", + "family:ssrf" + ], + "title": "Merge output forwarded to external UNOCONV_URL service" + }, + { + "confidence": "low", + "evidence_level": "suspicion", + "finding_id": "tool-hunter:document_merge_service/conftest.py:24", + "function_name": null, + "line": 24, + "path": "document_merge_service/conftest.py", + "proposed_snippet": null, + "ranking_priority": 10.1065, + "rationale": "TestUser(AnonymousUser) sets is_authenticated True and admin_client uses client.force_authenticate(user=admin_user). This is test-only scaffolding, but it means the group/authorization paths exercised in test_template.py never go through BearerTokenAuthentication, leaving the real auth boundary under-tested.", + "reachability_conditions": [], + "reachability_evidence": [], + "reachability_status": "unknown", + "rung": "suspicion", + "severity": "medium", + "status": "enabled", + "tags": [ + "tool-hunter", + "family:test-coverage" + ], + "title": "TestUser bypasses real authentication for admin client" + }, + { + "confidence": "low", + "evidence_level": "suspicion", + "finding_id": "tool-hunter:document_merge_service/settings.py:26", + "function_name": null, + "line": 26, + "path": "document_merge_service/settings.py", + "proposed_snippet": null, + "ranking_priority": 15.1654, + "rationale": "SECRET_KEY = env.str(\"SECRET_KEY\", default=default(\"uuuuuuuuuu\")). The default() helper returns default_prod when ENV == \"production\", and ENV defaults to \"production\". So if SECRET_KEY is unset in production, the app silently uses the weak literal \"uuuuuuuuuu\" instead of failing. This is a predictable signing key for Django sessions/CSRF.", + "reachability_conditions": [], + "reachability_evidence": [], + "reachability_status": "unknown", + "rung": "suspicion", + "severity": "medium", + "status": "enabled", + "tags": [ + "tool-hunter", + "family:secret-management" + ], + "title": "Hardcoded fallback SECRET_KEY in production default" + }, + { + "confidence": "low", + "evidence_level": "suspicion", + "finding_id": "tool-hunter:document_merge_service/settings.py:28", + "function_name": null, + "line": 28, + "path": "document_merge_service/settings.py", + "proposed_snippet": null, + "ranking_priority": 15.1654, + "rationale": "ALLOWED_HOSTS = env.list(\"ALLOWED_HOSTS\", default=default([\"*\"])) \u2014 with ENV defaulting to production, the production default is [\"*\"], disabling Django host-header validation. Combined with DEBUG defaulting to False in prod, this permits arbitrary Host headers.", + "reachability_conditions": [], + "reachability_evidence": [], + "reachability_status": "unknown", + "rung": "suspicion", + "severity": "medium", + "status": "enabled", + "tags": [ + "tool-hunter", + "family:misconfiguration" + ], + "title": "ALLOWED_HOSTS defaults to wildcard in production" + } + ] +} diff --git a/benchmarks/measurements/2026-10-12-hunter-fair-targets/malla-findings.json b/benchmarks/measurements/2026-10-12-hunter-fair-targets/malla-findings.json new file mode 100644 index 0000000..0d88ace --- /dev/null +++ b/benchmarks/measurements/2026-10-12-hunter-fair-targets/malla-findings.json @@ -0,0 +1,1293 @@ +{ + "findings": [ + { + "confidence": "medium", + "evidence_level": "static_corroboration", + "finding_id": "python-sql-injection:src/malla/database/repositories.py:52", + "function_name": null, + "line": 52, + "path": "src/malla/database/repositories.py", + "proposed_snippet": null, + "ranking_priority": 4.3, + "rationale": "Static pattern python-sql-injection (CWE-89) matched .execute on line 52; manual verification still required.", + "reachability_conditions": [], + "reachability_evidence": [ + { + "access_declared": false, + "access_evidence": [ + "network entry file tag" + ], + "access_level": "public", + "conditions": [], + "defines_handler": true, + "end_line": null, + "function_name": null, + "kind": "route", + "line": null, + "name": "network_surface", + "path": "src/malla/database/repositories.py", + "provenance": "entrypoint:route:public", + "rationale": "route surface classified as public at network_request", + "registered_by": "", + "trust_boundary": "network_request" + }, + { + "access_declared": false, + "access_evidence": [ + "parser/deserialization file tag" + ], + "access_level": "public", + "conditions": [], + "defines_handler": true, + "end_line": null, + "function_name": null, + "kind": "parser", + "line": null, + "name": "parser_input", + "path": "src/malla/database/repositories.py", + "provenance": "entrypoint:parser:public", + "rationale": "parser surface classified as public at attacker_controlled_input", + "registered_by": "", + "trust_boundary": "attacker_controlled_input" + } + ], + "reachability_status": "inferred-file-surface", + "rung": "suspicion", + "severity": "high", + "status": "enabled", + "tags": [ + "crypto", + "injection", + "network_entry", + "parser" + ], + "title": "Python SQL string composition needs review" + }, + { + "confidence": "medium", + "evidence_level": "static_corroboration", + "finding_id": "python-sql-injection:src/malla/database/repositories.py:73", + "function_name": null, + "line": 73, + "path": "src/malla/database/repositories.py", + "proposed_snippet": null, + "ranking_priority": 4.3, + "rationale": "Static pattern python-sql-injection (CWE-89) matched .execute on line 73; manual verification still required.", + "reachability_conditions": [], + "reachability_evidence": [ + { + "access_declared": false, + "access_evidence": [ + "network entry file tag" + ], + "access_level": "public", + "conditions": [], + "defines_handler": true, + "end_line": null, + "function_name": null, + "kind": "route", + "line": null, + "name": "network_surface", + "path": "src/malla/database/repositories.py", + "provenance": "entrypoint:route:public", + "rationale": "route surface classified as public at network_request", + "registered_by": "", + "trust_boundary": "network_request" + }, + { + "access_declared": false, + "access_evidence": [ + "parser/deserialization file tag" + ], + "access_level": "public", + "conditions": [], + "defines_handler": true, + "end_line": null, + "function_name": null, + "kind": "parser", + "line": null, + "name": "parser_input", + "path": "src/malla/database/repositories.py", + "provenance": "entrypoint:parser:public", + "rationale": "parser surface classified as public at attacker_controlled_input", + "registered_by": "", + "trust_boundary": "attacker_controlled_input" + } + ], + "reachability_status": "inferred-file-surface", + "rung": "suspicion", + "severity": "high", + "status": "enabled", + "tags": [ + "crypto", + "injection", + "network_entry", + "parser" + ], + "title": "Python SQL string composition needs review" + }, + { + "confidence": "medium", + "evidence_level": "static_corroboration", + "finding_id": "python-sql-injection:src/malla/database/repositories.py:80", + "function_name": null, + "line": 80, + "path": "src/malla/database/repositories.py", + "proposed_snippet": null, + "ranking_priority": 4.3, + "rationale": "Static pattern python-sql-injection (CWE-89) matched .execute on line 80; manual verification still required.", + "reachability_conditions": [], + "reachability_evidence": [ + { + "access_declared": false, + "access_evidence": [ + "network entry file tag" + ], + "access_level": "public", + "conditions": [], + "defines_handler": true, + "end_line": null, + "function_name": null, + "kind": "route", + "line": null, + "name": "network_surface", + "path": "src/malla/database/repositories.py", + "provenance": "entrypoint:route:public", + "rationale": "route surface classified as public at network_request", + "registered_by": "", + "trust_boundary": "network_request" + }, + { + "access_declared": false, + "access_evidence": [ + "parser/deserialization file tag" + ], + "access_level": "public", + "conditions": [], + "defines_handler": true, + "end_line": null, + "function_name": null, + "kind": "parser", + "line": null, + "name": "parser_input", + "path": "src/malla/database/repositories.py", + "provenance": "entrypoint:parser:public", + "rationale": "parser surface classified as public at attacker_controlled_input", + "registered_by": "", + "trust_boundary": "attacker_controlled_input" + } + ], + "reachability_status": "inferred-file-surface", + "rung": "suspicion", + "severity": "high", + "status": "enabled", + "tags": [ + "crypto", + "injection", + "network_entry", + "parser" + ], + "title": "Python SQL string composition needs review" + }, + { + "confidence": "medium", + "evidence_level": "static_corroboration", + "finding_id": "python-sql-injection:src/malla/database/repositories.py:3024", + "function_name": null, + "line": 3024, + "path": "src/malla/database/repositories.py", + "proposed_snippet": null, + "ranking_priority": 4.3, + "rationale": "Static pattern python-sql-injection (CWE-89) matched .execute on line 3024; manual verification still required.", + "reachability_conditions": [], + "reachability_evidence": [ + { + "access_declared": false, + "access_evidence": [ + "network entry file tag" + ], + "access_level": "public", + "conditions": [], + "defines_handler": true, + "end_line": null, + "function_name": null, + "kind": "route", + "line": null, + "name": "network_surface", + "path": "src/malla/database/repositories.py", + "provenance": "entrypoint:route:public", + "rationale": "route surface classified as public at network_request", + "registered_by": "", + "trust_boundary": "network_request" + }, + { + "access_declared": false, + "access_evidence": [ + "parser/deserialization file tag" + ], + "access_level": "public", + "conditions": [], + "defines_handler": true, + "end_line": null, + "function_name": null, + "kind": "parser", + "line": null, + "name": "parser_input", + "path": "src/malla/database/repositories.py", + "provenance": "entrypoint:parser:public", + "rationale": "parser surface classified as public at attacker_controlled_input", + "registered_by": "", + "trust_boundary": "attacker_controlled_input" + } + ], + "reachability_status": "inferred-file-surface", + "rung": "suspicion", + "severity": "high", + "status": "enabled", + "tags": [ + "crypto", + "injection", + "network_entry", + "parser" + ], + "title": "Python SQL string composition needs review" + }, + { + "confidence": "medium", + "evidence_level": "static_corroboration", + "finding_id": "python-sql-injection:src/malla/mqtt_capture.py:344", + "function_name": null, + "line": 344, + "path": "src/malla/mqtt_capture.py", + "proposed_snippet": null, + "ranking_priority": 1.5, + "rationale": "Static pattern python-sql-injection (CWE-89) matched .execute on line 344; manual verification still required.", + "reachability_conditions": [], + "reachability_evidence": [], + "reachability_status": "unknown", + "rung": "suspicion", + "severity": "high", + "status": "enabled", + "tags": [ + "auth_boundary", + "crypto", + "injection", + "network_entry", + "parser" + ], + "title": "Python SQL string composition needs review" + }, + { + "confidence": "medium", + "evidence_level": "static_corroboration", + "finding_id": "python-sql-injection:src/malla/services/analytics_service.py:182", + "function_name": null, + "line": 182, + "path": "src/malla/services/analytics_service.py", + "proposed_snippet": null, + "ranking_priority": 1.0, + "rationale": "Static pattern python-sql-injection (CWE-89) matched .execute on line 182; manual verification still required.", + "reachability_conditions": [], + "reachability_evidence": [], + "reachability_status": "unknown", + "rung": "suspicion", + "severity": "high", + "status": "enabled", + "tags": [ + "injection" + ], + "title": "Python SQL string composition needs review" + }, + { + "confidence": "medium", + "evidence_level": "static_corroboration", + "finding_id": "python-sql-injection:src/malla/services/analytics_service.py:250", + "function_name": null, + "line": 250, + "path": "src/malla/services/analytics_service.py", + "proposed_snippet": null, + "ranking_priority": 1.0, + "rationale": "Static pattern python-sql-injection (CWE-89) matched .execute on line 250; manual verification still required.", + "reachability_conditions": [], + "reachability_evidence": [], + "reachability_status": "unknown", + "rung": "suspicion", + "severity": "high", + "status": "enabled", + "tags": [ + "injection" + ], + "title": "Python SQL string composition needs review" + }, + { + "confidence": "medium", + "evidence_level": "static_corroboration", + "finding_id": "python-sql-injection:src/malla/services/analytics_service.py:443", + "function_name": null, + "line": 443, + "path": "src/malla/services/analytics_service.py", + "proposed_snippet": null, + "ranking_priority": 1.0, + "rationale": "Static pattern python-sql-injection (CWE-89) matched .execute on line 443; manual verification still required.", + "reachability_conditions": [], + "reachability_evidence": [], + "reachability_status": "unknown", + "rung": "suspicion", + "severity": "high", + "status": "enabled", + "tags": [ + "injection" + ], + "title": "Python SQL string composition needs review" + }, + { + "confidence": "medium", + "evidence_level": "static_corroboration", + "finding_id": "python-sql-injection:src/malla/services/analytics_service.py:493", + "function_name": null, + "line": 493, + "path": "src/malla/services/analytics_service.py", + "proposed_snippet": null, + "ranking_priority": 1.0, + "rationale": "Static pattern python-sql-injection (CWE-89) matched .execute on line 493; manual verification still required.", + "reachability_conditions": [], + "reachability_evidence": [], + "reachability_status": "unknown", + "rung": "suspicion", + "severity": "high", + "status": "enabled", + "tags": [ + "injection" + ], + "title": "Python SQL string composition needs review" + }, + { + "confidence": "medium", + "evidence_level": "static_corroboration", + "finding_id": "python-sql-injection:src/malla/services/gateway_service.py:205", + "function_name": null, + "line": 205, + "path": "src/malla/services/gateway_service.py", + "proposed_snippet": null, + "ranking_priority": 1.0, + "rationale": "Static pattern python-sql-injection (CWE-89) matched .execute on line 205; manual verification still required.", + "reachability_conditions": [], + "reachability_evidence": [], + "reachability_status": "unknown", + "rung": "suspicion", + "severity": "high", + "status": "enabled", + "tags": [ + "injection" + ], + "title": "Python SQL string composition needs review" + }, + { + "confidence": "medium", + "evidence_level": "static_corroboration", + "finding_id": "python-sql-injection:src/malla/utils/node_utils.py:225", + "function_name": null, + "line": 225, + "path": "src/malla/utils/node_utils.py", + "proposed_snippet": null, + "ranking_priority": 1.0, + "rationale": "Static pattern python-sql-injection (CWE-89) matched .execute on line 225; manual verification still required.", + "reachability_conditions": [], + "reachability_evidence": [], + "reachability_status": "unknown", + "rung": "suspicion", + "severity": "high", + "status": "enabled", + "tags": [ + "injection" + ], + "title": "Python SQL string composition needs review" + }, + { + "confidence": "medium", + "evidence_level": "static_corroboration", + "finding_id": "python-sql-injection:src/malla/utils/node_utils.py:305", + "function_name": null, + "line": 305, + "path": "src/malla/utils/node_utils.py", + "proposed_snippet": null, + "ranking_priority": 1.0, + "rationale": "Static pattern python-sql-injection (CWE-89) matched .execute on line 305; manual verification still required.", + "reachability_conditions": [], + "reachability_evidence": [], + "reachability_status": "unknown", + "rung": "suspicion", + "severity": "high", + "status": "enabled", + "tags": [ + "injection" + ], + "title": "Python SQL string composition needs review" + }, + { + "confidence": "medium", + "evidence_level": "static_corroboration", + "finding_id": "python-ssti:tests/conftest/test_server.py:97", + "function_name": null, + "line": 97, + "path": "tests/conftest/test_server.py", + "proposed_snippet": null, + "ranking_priority": 1.5, + "rationale": "Static pattern python-ssti (CWE-94) matched render_template_string on line 97; manual verification still required.", + "reachability_conditions": [], + "reachability_evidence": [], + "reachability_status": "unknown", + "rung": "suspicion", + "severity": "medium", + "status": "enabled", + "tags": [ + "auth_boundary", + "filesystem_entry", + "injection", + "network_entry" + ], + "title": "Python server-side template injection needs review" + }, + { + "confidence": "medium", + "evidence_level": "static_corroboration", + "finding_id": "overlay-coverage:run_tests.py:19:os.system", + "function_name": null, + "line": 19, + "path": "run_tests.py", + "proposed_snippet": null, + "ranking_priority": 0.0, + "rationale": "uninventoried sink os.system reached from parameter", + "reachability_conditions": [], + "reachability_evidence": [], + "reachability_status": "unknown", + "rung": "suspicion", + "severity": "critical", + "status": "enabled", + "tags": [ + "overlay-originated" + ], + "title": "Overlay coverage: os.system" + }, + { + "confidence": "medium", + "evidence_level": "static_corroboration", + "finding_id": "overlay-coverage:src/malla/database/packet_repository_optimized.py:144:execute", + "function_name": null, + "line": 144, + "path": "src/malla/database/packet_repository_optimized.py", + "proposed_snippet": null, + "ranking_priority": 0.0, + "rationale": "uninventoried sink execute reached from parameter", + "reachability_conditions": [], + "reachability_evidence": [], + "reachability_status": "unknown", + "rung": "suspicion", + "severity": "high", + "status": "enabled", + "tags": [ + "overlay-originated" + ], + "title": "Overlay coverage: execute" + }, + { + "confidence": "medium", + "evidence_level": "static_corroboration", + "finding_id": "overlay-coverage:src/malla/database/packet_repository_optimized.py:316:execute", + "function_name": null, + "line": 316, + "path": "src/malla/database/packet_repository_optimized.py", + "proposed_snippet": null, + "ranking_priority": 0.0, + "rationale": "uninventoried sink execute reached from parameter", + "reachability_conditions": [], + "reachability_evidence": [], + "reachability_status": "unknown", + "rung": "suspicion", + "severity": "high", + "status": "enabled", + "tags": [ + "overlay-originated" + ], + "title": "Overlay coverage: execute" + }, + { + "confidence": "medium", + "evidence_level": "static_corroboration", + "finding_id": "overlay-coverage:src/malla/database/repositories.py:301:execute", + "function_name": null, + "line": 301, + "path": "src/malla/database/repositories.py", + "proposed_snippet": null, + "ranking_priority": 0.0, + "rationale": "uninventoried sink execute reached from parameter", + "reachability_conditions": [], + "reachability_evidence": [], + "reachability_status": "unknown", + "rung": "suspicion", + "severity": "high", + "status": "enabled", + "tags": [ + "overlay-originated" + ], + "title": "Overlay coverage: execute" + }, + { + "confidence": "medium", + "evidence_level": "static_corroboration", + "finding_id": "overlay-coverage:src/malla/database/repositories.py:536:execute", + "function_name": null, + "line": 536, + "path": "src/malla/database/repositories.py", + "proposed_snippet": null, + "ranking_priority": 0.0, + "rationale": "uninventoried sink execute reached from parameter", + "reachability_conditions": [], + "reachability_evidence": [], + "reachability_status": "unknown", + "rung": "suspicion", + "severity": "high", + "status": "enabled", + "tags": [ + "overlay-originated" + ], + "title": "Overlay coverage: execute" + }, + { + "confidence": "medium", + "evidence_level": "static_corroboration", + "finding_id": "overlay-coverage:src/malla/database/repositories.py:790:execute", + "function_name": null, + "line": 790, + "path": "src/malla/database/repositories.py", + "proposed_snippet": null, + "ranking_priority": 0.0, + "rationale": "uninventoried sink execute reached from parameter", + "reachability_conditions": [], + "reachability_evidence": [], + "reachability_status": "unknown", + "rung": "suspicion", + "severity": "high", + "status": "enabled", + "tags": [ + "overlay-originated" + ], + "title": "Overlay coverage: execute" + }, + { + "confidence": "medium", + "evidence_level": "static_corroboration", + "finding_id": "overlay-coverage:src/malla/database/repositories.py:1089:execute", + "function_name": null, + "line": 1089, + "path": "src/malla/database/repositories.py", + "proposed_snippet": null, + "ranking_priority": 0.0, + "rationale": "uninventoried sink execute reached from parameter", + "reachability_conditions": [], + "reachability_evidence": [], + "reachability_status": "unknown", + "rung": "suspicion", + "severity": "high", + "status": "enabled", + "tags": [ + "overlay-originated" + ], + "title": "Overlay coverage: execute" + }, + { + "confidence": "medium", + "evidence_level": "static_corroboration", + "finding_id": "overlay-coverage:src/malla/database/repositories.py:2133:execute", + "function_name": null, + "line": 2133, + "path": "src/malla/database/repositories.py", + "proposed_snippet": null, + "ranking_priority": 0.0, + "rationale": "uninventoried sink execute reached from parameter", + "reachability_conditions": [], + "reachability_evidence": [], + "reachability_status": "unknown", + "rung": "suspicion", + "severity": "high", + "status": "enabled", + "tags": [ + "overlay-originated" + ], + "title": "Overlay coverage: execute" + }, + { + "confidence": "medium", + "evidence_level": "static_corroboration", + "finding_id": "overlay-coverage:src/malla/database/repositories.py:2739:execute", + "function_name": null, + "line": 2739, + "path": "src/malla/database/repositories.py", + "proposed_snippet": null, + "ranking_priority": 0.0, + "rationale": "uninventoried sink execute reached from parameter", + "reachability_conditions": [], + "reachability_evidence": [], + "reachability_status": "unknown", + "rung": "suspicion", + "severity": "high", + "status": "enabled", + "tags": [ + "overlay-originated" + ], + "title": "Overlay coverage: execute" + }, + { + "confidence": "medium", + "evidence_level": "static_corroboration", + "finding_id": "overlay-coverage:src/malla/database/repositories.py:3059:execute", + "function_name": null, + "line": 3059, + "path": "src/malla/database/repositories.py", + "proposed_snippet": null, + "ranking_priority": 0.0, + "rationale": "uninventoried sink execute reached from parameter", + "reachability_conditions": [], + "reachability_evidence": [], + "reachability_status": "unknown", + "rung": "suspicion", + "severity": "high", + "status": "enabled", + "tags": [ + "overlay-originated" + ], + "title": "Overlay coverage: execute" + }, + { + "confidence": "medium", + "evidence_level": "static_corroboration", + "finding_id": "overlay-coverage:src/malla/services/analytics_service.py:139:execute", + "function_name": null, + "line": 139, + "path": "src/malla/services/analytics_service.py", + "proposed_snippet": null, + "ranking_priority": 0.0, + "rationale": "uninventoried sink execute reached from parameter", + "reachability_conditions": [], + "reachability_evidence": [], + "reachability_status": "unknown", + "rung": "suspicion", + "severity": "high", + "status": "enabled", + "tags": [ + "overlay-originated" + ], + "title": "Overlay coverage: execute" + }, + { + "confidence": "medium", + "evidence_level": "static_corroboration", + "finding_id": "overlay-coverage:src/malla/services/analytics_service.py:343:execute", + "function_name": null, + "line": 343, + "path": "src/malla/services/analytics_service.py", + "proposed_snippet": null, + "ranking_priority": 0.0, + "rationale": "uninventoried sink execute reached from parameter", + "reachability_conditions": [], + "reachability_evidence": [], + "reachability_status": "unknown", + "rung": "suspicion", + "severity": "high", + "status": "enabled", + "tags": [ + "overlay-originated" + ], + "title": "Overlay coverage: execute" + }, + { + "confidence": "medium", + "evidence_level": "static_corroboration", + "finding_id": "overlay-coverage:src/malla/static/js/direct_receptions.js:73:outbound.request", + "function_name": null, + "line": 73, + "path": "src/malla/static/js/direct_receptions.js", + "proposed_snippet": null, + "ranking_priority": 0.0, + "rationale": "uninventoried sink outbound.request reached from parameter", + "reachability_conditions": [], + "reachability_evidence": [], + "reachability_status": "unknown", + "rung": "suspicion", + "severity": "medium", + "status": "enabled", + "tags": [ + "overlay-originated" + ], + "title": "Overlay coverage: outbound.request" + }, + { + "confidence": "medium", + "evidence_level": "static_corroboration", + "finding_id": "overlay-coverage:src/malla/static/js/node-cache.js:117:outbound.request", + "function_name": null, + "line": 117, + "path": "src/malla/static/js/node-cache.js", + "proposed_snippet": null, + "ranking_priority": 0.0, + "rationale": "uninventoried sink outbound.request reached from parameter", + "reachability_conditions": [], + "reachability_evidence": [], + "reachability_status": "unknown", + "rung": "suspicion", + "severity": "medium", + "status": "enabled", + "tags": [ + "overlay-originated" + ], + "title": "Overlay coverage: outbound.request" + }, + { + "confidence": "medium", + "evidence_level": "static_corroboration", + "finding_id": "overlay-coverage:src/malla/static/js/node-picker.js:581:outbound.request", + "function_name": null, + "line": 581, + "path": "src/malla/static/js/node-picker.js", + "proposed_snippet": null, + "ranking_priority": 0.0, + "rationale": "uninventoried sink outbound.request reached from parameter", + "reachability_conditions": [], + "reachability_evidence": [], + "reachability_status": "unknown", + "rung": "suspicion", + "severity": "medium", + "status": "enabled", + "tags": [ + "overlay-originated" + ], + "title": "Overlay coverage: outbound.request" + }, + { + "confidence": "medium", + "evidence_level": "static_corroboration", + "finding_id": "overlay-coverage:src/malla/static/js/url-filter-manager.js:41:prototype.merge", + "function_name": null, + "line": 41, + "path": "src/malla/static/js/url-filter-manager.js", + "proposed_snippet": null, + "ranking_priority": 0.0, + "rationale": "uninventoried sink prototype.merge reached from parameter", + "reachability_conditions": [], + "reachability_evidence": [], + "reachability_status": "unknown", + "rung": "suspicion", + "severity": "info", + "status": "enabled", + "tags": [ + "overlay-originated" + ], + "title": "Overlay coverage: prototype.merge" + }, + { + "confidence": "medium", + "evidence_level": "static_corroboration", + "finding_id": "overlay-coverage:src/malla/static/js/url-filter-manager.js:110:outbound.request", + "function_name": null, + "line": 110, + "path": "src/malla/static/js/url-filter-manager.js", + "proposed_snippet": null, + "ranking_priority": 0.0, + "rationale": "uninventoried sink outbound.request reached from parameter", + "reachability_conditions": [], + "reachability_evidence": [], + "reachability_status": "unknown", + "rung": "suspicion", + "severity": "medium", + "status": "enabled", + "tags": [ + "overlay-originated" + ], + "title": "Overlay coverage: outbound.request" + }, + { + "confidence": "medium", + "evidence_level": "static_corroboration", + "finding_id": "overlay-coverage:src/malla/static/js/url-filter-manager.js:171:outbound.request", + "function_name": null, + "line": 171, + "path": "src/malla/static/js/url-filter-manager.js", + "proposed_snippet": null, + "ranking_priority": 0.0, + "rationale": "uninventoried sink outbound.request reached from parameter", + "reachability_conditions": [], + "reachability_evidence": [], + "reachability_status": "unknown", + "rung": "suspicion", + "severity": "medium", + "status": "enabled", + "tags": [ + "overlay-originated" + ], + "title": "Overlay coverage: outbound.request" + }, + { + "confidence": "medium", + "evidence_level": "static_corroboration", + "finding_id": "overlay-coverage:src/malla/static/js/url-filter-manager.js:183:outbound.request", + "function_name": null, + "line": 183, + "path": "src/malla/static/js/url-filter-manager.js", + "proposed_snippet": null, + "ranking_priority": 0.0, + "rationale": "uninventoried sink outbound.request reached from parameter", + "reachability_conditions": [], + "reachability_evidence": [], + "reachability_status": "unknown", + "rung": "suspicion", + "severity": "medium", + "status": "enabled", + "tags": [ + "overlay-originated" + ], + "title": "Overlay coverage: outbound.request" + }, + { + "confidence": "medium", + "evidence_level": "static_corroboration", + "finding_id": "overlay-coverage:src/malla/static/js/url-filter-manager.js:279:prototype.merge", + "function_name": null, + "line": 279, + "path": "src/malla/static/js/url-filter-manager.js", + "proposed_snippet": null, + "ranking_priority": 0.0, + "rationale": "uninventoried sink prototype.merge reached from parameter", + "reachability_conditions": [], + "reachability_evidence": [], + "reachability_status": "unknown", + "rung": "suspicion", + "severity": "info", + "status": "enabled", + "tags": [ + "overlay-originated" + ], + "title": "Overlay coverage: prototype.merge" + }, + { + "confidence": "medium", + "evidence_level": "static_corroboration", + "finding_id": "overlay-coverage:src/malla/web_ui.py:207:html.unescaped", + "function_name": null, + "line": 207, + "path": "src/malla/web_ui.py", + "proposed_snippet": null, + "ranking_priority": 0.0, + "rationale": "uninventoried sink html.unescaped reached from parameter", + "reachability_conditions": [], + "reachability_evidence": [], + "reachability_status": "unknown", + "rung": "suspicion", + "severity": "medium", + "status": "enabled", + "tags": [ + "overlay-originated" + ], + "title": "Overlay coverage: html.unescaped" + }, + { + "confidence": "medium", + "evidence_level": "static_corroboration", + "finding_id": "overlay-coverage:tests/conftest.py:119:path.open", + "function_name": null, + "line": 119, + "path": "tests/conftest.py", + "proposed_snippet": null, + "ranking_priority": 0.0, + "rationale": "uninventoried sink path.open reached from parameter", + "reachability_conditions": [], + "reachability_evidence": [], + "reachability_status": "unknown", + "rung": "suspicion", + "severity": "medium", + "status": "enabled", + "tags": [ + "overlay-originated" + ], + "title": "Overlay coverage: path.open" + }, + { + "confidence": "medium", + "evidence_level": "static_corroboration", + "finding_id": "overlay-coverage:tests/conftest/test_server.py:67:path.open", + "function_name": null, + "line": 67, + "path": "tests/conftest/test_server.py", + "proposed_snippet": null, + "ranking_priority": 0.0, + "rationale": "uninventoried sink path.open reached from parameter", + "reachability_conditions": [], + "reachability_evidence": [], + "reachability_status": "unknown", + "rung": "suspicion", + "severity": "medium", + "status": "enabled", + "tags": [ + "overlay-originated" + ], + "title": "Overlay coverage: path.open" + }, + { + "confidence": "low", + "evidence_level": "suspicion", + "finding_id": "obligation:protected_read:src/malla/routes/api_routes.py:217:identity_constraint", + "function_name": "api_nodes", + "line": 217, + "path": "src/malla/routes/api_routes.py", + "proposed_snippet": null, + "ranking_priority": 0.3, + "rationale": "protected_read on a resource in api_nodes is reached without a dominating identity_constraint. Why the obligation exists: the operation fact alone (function-local mode). Label: function_local.", + "reachability_conditions": [], + "reachability_evidence": [], + "reachability_status": "unknown", + "rung": "suspicion", + "severity": "high", + "status": "enabled", + "tags": [ + "obligation:protected_read", + "discharger:identity_constraint", + "obligation_evidence:function_local" + ], + "title": "Obligation not discharged: protected_read on a resource without identity_constraint" + }, + { + "confidence": "low", + "evidence_level": "suspicion", + "finding_id": "obligation:protected_read:src/malla/routes/api_routes.py:267:identity_constraint", + "function_name": "api_nodes_search", + "line": 267, + "path": "src/malla/routes/api_routes.py", + "proposed_snippet": null, + "ranking_priority": 0.3, + "rationale": "protected_read on a resource in api_nodes_search is reached without a dominating identity_constraint. Why the obligation exists: the operation fact alone (function-local mode). Label: function_local.", + "reachability_conditions": [], + "reachability_evidence": [], + "reachability_status": "unknown", + "rung": "suspicion", + "severity": "high", + "status": "enabled", + "tags": [ + "obligation:protected_read", + "discharger:identity_constraint", + "obligation_evidence:function_local" + ], + "title": "Obligation not discharged: protected_read on a resource without identity_constraint" + }, + { + "confidence": "low", + "evidence_level": "suspicion", + "finding_id": "obligation:protected_read:src/malla/routes/api_routes.py:374:identity_constraint", + "function_name": "api_gateways_search", + "line": 374, + "path": "src/malla/routes/api_routes.py", + "proposed_snippet": null, + "ranking_priority": 0.3, + "rationale": "protected_read on a resource in api_gateways_search is reached without a dominating identity_constraint. Why the obligation exists: the operation fact alone (function-local mode). Label: function_local.", + "reachability_conditions": [], + "reachability_evidence": [], + "reachability_status": "unknown", + "rung": "suspicion", + "severity": "high", + "status": "enabled", + "tags": [ + "obligation:protected_read", + "discharger:identity_constraint", + "obligation_evidence:function_local" + ], + "title": "Obligation not discharged: protected_read on a resource without identity_constraint" + }, + { + "confidence": "low", + "evidence_level": "suspicion", + "finding_id": "obligation:protected_read:src/malla/routes/api_routes.py:386:identity_constraint", + "function_name": "api_gateways_search", + "line": 386, + "path": "src/malla/routes/api_routes.py", + "proposed_snippet": null, + "ranking_priority": 0.3, + "rationale": "protected_read on a resource in api_gateways_search is reached without a dominating identity_constraint. Why the obligation exists: the operation fact alone (function-local mode). Label: function_local.", + "reachability_conditions": [], + "reachability_evidence": [], + "reachability_status": "unknown", + "rung": "suspicion", + "severity": "high", + "status": "enabled", + "tags": [ + "obligation:protected_read", + "discharger:identity_constraint", + "obligation_evidence:function_local" + ], + "title": "Obligation not discharged: protected_read on a resource without identity_constraint" + }, + { + "confidence": "low", + "evidence_level": "suspicion", + "finding_id": "obligation:protected_read:src/malla/routes/api_routes.py:895:identity_constraint", + "function_name": "api_traceroute_hops_nodes", + "line": 895, + "path": "src/malla/routes/api_routes.py", + "proposed_snippet": null, + "ranking_priority": 0.3, + "rationale": "protected_read on a resource in api_traceroute_hops_nodes is reached without a dominating identity_constraint. Why the obligation exists: the operation fact alone (function-local mode). Label: function_local.", + "reachability_conditions": [], + "reachability_evidence": [], + "reachability_status": "unknown", + "rung": "suspicion", + "severity": "high", + "status": "enabled", + "tags": [ + "obligation:protected_read", + "discharger:identity_constraint", + "obligation_evidence:function_local" + ], + "title": "Obligation not discharged: protected_read on a resource without identity_constraint" + }, + { + "confidence": "low", + "evidence_level": "suspicion", + "finding_id": "obligation:protected_read:src/malla/routes/api_routes.py:896:identity_constraint", + "function_name": "api_traceroute_hops_nodes", + "line": 896, + "path": "src/malla/routes/api_routes.py", + "proposed_snippet": null, + "ranking_priority": 0.3, + "rationale": "protected_read on a resource in api_traceroute_hops_nodes is reached without a dominating identity_constraint. Why the obligation exists: the operation fact alone (function-local mode). Label: function_local.", + "reachability_conditions": [], + "reachability_evidence": [], + "reachability_status": "unknown", + "rung": "suspicion", + "severity": "high", + "status": "enabled", + "tags": [ + "obligation:protected_read", + "discharger:identity_constraint", + "obligation_evidence:function_local" + ], + "title": "Obligation not discharged: protected_read on a resource without identity_constraint" + }, + { + "confidence": "low", + "evidence_level": "suspicion", + "finding_id": "tool-hunter:src/malla/config.py:88", + "function_name": null, + "line": 88, + "path": "src/malla/config.py", + "proposed_snippet": null, + "ranking_priority": 0.0, + "rationale": "Comment at line 88 states 'Basic builtin types are fine to eval() in this restricted context.' eval() on configuration-derived strings is a code-execution sink if the config file or environment can be influenced. Confirmed the eval usage exists; the exact coercion scope was not fully read.", + "reachability_conditions": [], + "reachability_evidence": [], + "reachability_status": "unknown", + "rung": "suspicion", + "severity": "medium", + "status": "enabled", + "tags": [ + "tool-hunter", + "family:code_execution" + ], + "title": "eval() used for config value coercion" + }, + { + "confidence": "low", + "evidence_level": "suspicion", + "finding_id": "tool-hunter:src/malla/database/packet_repository_optimized.py:311", + "function_name": null, + "line": 311, + "path": "src/malla/database/packet_repository_optimized.py", + "proposed_snippet": null, + "ranking_priority": 0.0, + "rationale": "Same pattern as repositories.py: order_by: str = \"timestamp\" (line 23) is interpolated into f\"... ORDER BY {order_by} {order_dir.upper()}\" (line 311), with where_clause also f-string built (lines 100-139, 295, 310). Identifier-position interpolation is injection-prone unless callers enforce an allowlist.", + "reachability_conditions": [], + "reachability_evidence": [], + "reachability_status": "unknown", + "rung": "suspicion", + "severity": "medium", + "status": "enabled", + "tags": [ + "tool-hunter", + "family:sql_injection" + ], + "title": "order_by/order_dir interpolated into SQL ORDER BY clause" + }, + { + "confidence": "low", + "evidence_level": "suspicion", + "finding_id": "tool-hunter:src/malla/database/repositories.py:152", + "function_name": null, + "line": 152, + "path": "src/malla/database/repositories.py", + "proposed_snippet": null, + "ranking_priority": 18.7715, + "rationale": "get_packets accepts order_by: str = \"timestamp\" and the query is built with f-string interpolation ({where_clause} and ORDER BY {order_by} {order_dir.upper()} at line 296 area). If order_by/order_dir reach this from request args without an allowlist, this is SQL injection via identifier position. Confirmed the f-string query construction exists in this file; the caller-side validation was not established by the tools.", + "reachability_conditions": [], + "reachability_evidence": [], + "reachability_status": "unknown", + "rung": "suspicion", + "severity": "medium", + "status": "enabled", + "tags": [ + "tool-hunter", + "family:sql_injection" + ], + "title": "order_by parameter interpolated into SQL ORDER BY clause" + }, + { + "confidence": "low", + "evidence_level": "suspicion", + "finding_id": "tool-hunter:src/malla/database/repositories.py:53", + "function_name": null, + "line": 53, + "path": "src/malla/database/repositories.py", + "proposed_snippet": null, + "ranking_priority": 18.7715, + "rationale": "get_stats builds gateway_filter = \" AND gateway_id = ?\" and interpolates it into an f-string query (lines 53, 74, 81). The fragment itself is a constant with a bound parameter, so this is low risk, but it is the same f-string SQL construction pattern flagged across the file and worth confirming no caller-controlled fragment is ever substituted.", + "reachability_conditions": [], + "reachability_evidence": [], + "reachability_status": "unknown", + "rung": "suspicion", + "severity": "medium", + "status": "enabled", + "tags": [ + "tool-hunter", + "family:sql_injection" + ], + "title": "f-string SQL with gateway_filter fragment in DashboardRepository.get_stats" + }, + { + "confidence": "low", + "evidence_level": "suspicion", + "finding_id": "tool-hunter:src/malla/mqtt_capture.py:157", + "function_name": null, + "line": 157, + "path": "src/malla/mqtt_capture.py", + "proposed_snippet": null, + "ranking_priority": 17.2018, + "rationale": "decrypt_packet constructs a Cipher (line 157) and decrypts attacker-reachable MQTT payloads (try_decrypt_mesh_packet, line 175; called from the capture loop around line 1063). Decrypted bytes are then ParseFromString'd as protobuf (line 241). This is the crypto/network_entry/parser hotspot; the decryption path handles untrusted input and feeds a protobuf parser.", + "reachability_conditions": [], + "reachability_evidence": [], + "reachability_status": "unknown", + "rung": "suspicion", + "severity": "medium", + "status": "enabled", + "tags": [ + "tool-hunter", + "family:crypto" + ], + "title": "AES decryption of network-received MQTT payloads" + }, + { + "confidence": "low", + "evidence_level": "suspicion", + "finding_id": "tool-hunter:src/malla/mqtt_capture.py:241", + "function_name": null, + "line": 241, + "path": "src/malla/mqtt_capture.py", + "proposed_snippet": null, + "ranking_priority": 17.2018, + "rationale": "decoded_data.ParseFromString(decrypted_payload) parses bytes derived from network MQTT traffic after decryption. Protobuf parsing of untrusted input is the parser sink in this high-complexity module; malformed input is caught (parse_error handling at line 260) but the parse itself operates on attacker-influenced bytes.", + "reachability_conditions": [], + "reachability_evidence": [], + "reachability_status": "unknown", + "rung": "suspicion", + "severity": "medium", + "status": "enabled", + "tags": [ + "tool-hunter", + "family:parser" + ], + "title": "Protobuf ParseFromString on decrypted network payload" + }, + { + "confidence": "low", + "evidence_level": "suspicion", + "finding_id": "tool-hunter:src/malla/routes/api_routes.py:88", + "function_name": null, + "line": 88, + "path": "src/malla/routes/api_routes.py", + "proposed_snippet": null, + "ranking_priority": 16.79, + "rationale": "api_analytics (line 88) reads gateway_id, from_node, hop_count from request.args and forwards to AnalyticsService.get_analytics_data. api_packets (line 111) similarly builds filters from request args. These are the network_entry points feeding the repository query builders flagged above; whether the args reach order_by/where fragments was not established by the tools.", + "reachability_conditions": [], + "reachability_evidence": [], + "reachability_status": "unknown", + "rung": "suspicion", + "severity": "medium", + "status": "enabled", + "tags": [ + "tool-hunter", + "family:sql_injection" + ], + "title": "api_analytics route passes request args into service layer" + }, + { + "confidence": "low", + "evidence_level": "suspicion", + "finding_id": "tool-hunter:src/malla/routes/packet_routes.py:1369", + "function_name": null, + "line": 1369, + "path": "src/malla/routes/packet_routes.py", + "proposed_snippet": null, + "ranking_priority": 17.7787, + "rationale": "packet_detail (line 1369) and packets (line 1347) are the flagged high-band functions in this 1202-LOC module. get_packet_details (line 24) executes parameterized SQL (WHERE id = ?, line 41) which is safe, but the route layer builds filters and calls repositories; the complexity/nesting (10) makes filter-construction paths worth auditing for identifier interpolation. Confirmed the route definitions and the parameterized query.", + "reachability_conditions": [], + "reachability_evidence": [], + "reachability_status": "unknown", + "rung": "suspicion", + "severity": "medium", + "status": "enabled", + "tags": [ + "tool-hunter", + "family:sql_injection" + ], + "title": "packet_detail route with high nesting and DB access" + }, + { + "confidence": "low", + "evidence_level": "suspicion", + "finding_id": "tool-hunter:src/malla/static/js/location-cache.js:18", + "function_name": null, + "line": 18, + "path": "src/malla/static/js/location-cache.js", + "proposed_snippet": null, + "ranking_priority": 18.0874, + "rationale": "location-cache.js parses localStorage (JSON.parse(raw), line 18) and returns parsed.locations, used for map/coordinate rendering. Same untrusted-storage deserialization pattern as node-cache.js; no validation of the parsed object shape beyond a typeof check.", + "reachability_conditions": [], + "reachability_evidence": [], + "reachability_status": "unknown", + "rung": "suspicion", + "severity": "medium", + "status": "enabled", + "tags": [ + "tool-hunter", + "family:untrusted_deserialization" + ], + "title": "JSON.parse of localStorage cache without schema validation" + }, + { + "confidence": "low", + "evidence_level": "suspicion", + "finding_id": "tool-hunter:src/malla/static/js/node-cache.js:19", + "function_name": null, + "line": 19, + "path": "src/malla/static/js/node-cache.js", + "proposed_snippet": null, + "ranking_priority": 18.2639, + "rationale": "node-cache.js parses localStorage (JSON.parse(raw), line 19) and returns parsed.nodes, which is then used by getNode/search and rendered elsewhere. localStorage is attacker-writable in XSS scenarios; the parsed node objects flow into UI rendering (innerHTML usage exists in sibling files like node-picker.js/modern-table.js). Deserialization of untrusted storage without validation.", + "reachability_conditions": [], + "reachability_evidence": [], + "reachability_status": "unknown", + "rung": "suspicion", + "severity": "medium", + "status": "enabled", + "tags": [ + "tool-hunter", + "family:untrusted_deserialization" + ], + "title": "JSON.parse of localStorage cache without schema validation" + } + ] +} diff --git a/benchmarks/measurements/2026-10-12-hunter-fair-targets/mcpremote-findings.json b/benchmarks/measurements/2026-10-12-hunter-fair-targets/mcpremote-findings.json new file mode 100644 index 0000000..db35398 --- /dev/null +++ b/benchmarks/measurements/2026-10-12-hunter-fair-targets/mcpremote-findings.json @@ -0,0 +1,611 @@ +{ + "findings": [ + { + "confidence": "medium", + "evidence_level": "static_corroboration", + "finding_id": "js-eval:src/lib/utils.test.ts:36", + "function_name": null, + "line": 36, + "path": "src/lib/utils.test.ts", + "proposed_snippet": null, + "ranking_priority": 3.8, + "rationale": "Static pattern js-eval (CWE-95) matched eval on line 36; manual verification still required.", + "reachability_conditions": [], + "reachability_evidence": [ + { + "access_declared": false, + "access_evidence": [ + "auth boundary file tag" + ], + "access_level": "authenticated", + "conditions": [], + "defines_handler": true, + "end_line": null, + "function_name": null, + "kind": "privileged_surface", + "line": null, + "name": "auth_boundary", + "path": "src/lib/utils.test.ts", + "provenance": "entrypoint:privileged_surface:authenticated", + "rationale": "privileged_surface surface classified as authenticated at identity_boundary", + "registered_by": "", + "trust_boundary": "identity_boundary" + }, + { + "access_declared": false, + "access_evidence": [ + "syscall/filesystem file tag" + ], + "access_level": "local-only", + "conditions": [], + "defines_handler": true, + "end_line": null, + "function_name": null, + "kind": "privileged_surface", + "line": null, + "name": "local_privileged_boundary", + "path": "src/lib/utils.test.ts", + "provenance": "entrypoint:privileged_surface:local-only", + "rationale": "privileged_surface surface classified as local-only at local_process", + "registered_by": "", + "trust_boundary": "local_process" + }, + { + "access_declared": false, + "access_evidence": [ + "network entry file tag" + ], + "access_level": "public", + "conditions": [], + "defines_handler": true, + "end_line": null, + "function_name": null, + "kind": "route", + "line": null, + "name": "network_surface", + "path": "src/lib/utils.test.ts", + "provenance": "entrypoint:route:public", + "rationale": "route surface classified as public at network_request", + "registered_by": "", + "trust_boundary": "network_request" + } + ], + "reachability_status": "inferred-file-surface", + "rung": "suspicion", + "severity": "critical", + "status": "enabled", + "tags": [ + "auth_boundary", + "crypto", + "filesystem_entry", + "network_entry", + "syscall_entry" + ], + "title": "Dynamic JavaScript evaluation needs review" + }, + { + "confidence": "medium", + "evidence_level": "static_corroboration", + "finding_id": "js-weak-hash:src/lib/utils.ts:705", + "function_name": null, + "line": 705, + "path": "src/lib/utils.ts", + "proposed_snippet": null, + "ranking_priority": 1.5, + "rationale": "Static pattern js-weak-hash (CWE-327) matched md5 on line 705; manual verification still required.", + "reachability_conditions": [], + "reachability_evidence": [], + "reachability_status": "unknown", + "rung": "suspicion", + "severity": "medium", + "status": "enabled", + "tags": [ + "auth_boundary", + "crypto", + "deserialization", + "filesystem_entry", + "network_entry", + "parser" + ], + "title": "JavaScript weak hash needs review" + }, + { + "confidence": "medium", + "evidence_level": "static_corroboration", + "finding_id": "overlay-coverage:src/lib/coordination.ts:63:outbound.request", + "function_name": null, + "line": 63, + "path": "src/lib/coordination.ts", + "proposed_snippet": null, + "ranking_priority": 0.0, + "rationale": "uninventoried sink outbound.request reached from parameter", + "reachability_conditions": [], + "reachability_evidence": [], + "reachability_status": "unknown", + "rung": "suspicion", + "severity": "medium", + "status": "enabled", + "tags": [ + "overlay-originated" + ], + "title": "Overlay coverage: outbound.request" + }, + { + "confidence": "medium", + "evidence_level": "static_corroboration", + "finding_id": "overlay-coverage:src/lib/coordination.ts:96:outbound.request", + "function_name": null, + "line": 96, + "path": "src/lib/coordination.ts", + "proposed_snippet": null, + "ranking_priority": 0.0, + "rationale": "uninventoried sink outbound.request reached from parameter", + "reachability_conditions": [], + "reachability_evidence": [], + "reachability_status": "unknown", + "rung": "suspicion", + "severity": "medium", + "status": "enabled", + "tags": [ + "overlay-originated" + ], + "title": "Overlay coverage: outbound.request" + }, + { + "confidence": "medium", + "evidence_level": "static_corroboration", + "finding_id": "overlay-coverage:src/lib/mcp-auth-config.ts:122:path.open", + "function_name": null, + "line": 122, + "path": "src/lib/mcp-auth-config.ts", + "proposed_snippet": null, + "ranking_priority": 0.0, + "rationale": "uninventoried sink path.open reached from parameter", + "reachability_conditions": [], + "reachability_evidence": [], + "reachability_status": "unknown", + "rung": "suspicion", + "severity": "medium", + "status": "enabled", + "tags": [ + "overlay-originated" + ], + "title": "Overlay coverage: path.open" + }, + { + "confidence": "medium", + "evidence_level": "static_corroboration", + "finding_id": "overlay-coverage:src/lib/mcp-auth-config.ts:143:path.open", + "function_name": null, + "line": 143, + "path": "src/lib/mcp-auth-config.ts", + "proposed_snippet": null, + "ranking_priority": 0.0, + "rationale": "uninventoried sink path.open reached from parameter", + "reachability_conditions": [], + "reachability_evidence": [], + "reachability_status": "unknown", + "rung": "suspicion", + "severity": "medium", + "status": "enabled", + "tags": [ + "overlay-originated" + ], + "title": "Overlay coverage: path.open" + }, + { + "confidence": "medium", + "evidence_level": "static_corroboration", + "finding_id": "overlay-coverage:src/lib/mcp-auth-config.ts:185:path.open", + "function_name": null, + "line": 185, + "path": "src/lib/mcp-auth-config.ts", + "proposed_snippet": null, + "ranking_priority": 0.0, + "rationale": "uninventoried sink path.open reached from parameter", + "reachability_conditions": [], + "reachability_evidence": [], + "reachability_status": "unknown", + "rung": "suspicion", + "severity": "medium", + "status": "enabled", + "tags": [ + "overlay-originated" + ], + "title": "Overlay coverage: path.open" + }, + { + "confidence": "medium", + "evidence_level": "static_corroboration", + "finding_id": "overlay-coverage:src/lib/utils.ts:202:outbound.request", + "function_name": null, + "line": 202, + "path": "src/lib/utils.ts", + "proposed_snippet": null, + "ranking_priority": 0.0, + "rationale": "uninventoried sink outbound.request reached from parameter", + "reachability_conditions": [], + "reachability_evidence": [], + "reachability_status": "unknown", + "rung": "suspicion", + "severity": "medium", + "status": "enabled", + "tags": [ + "overlay-originated" + ], + "title": "Overlay coverage: outbound.request" + }, + { + "confidence": "medium", + "evidence_level": "static_corroboration", + "finding_id": "overlay-coverage:src/lib/utils.ts:588:path.open", + "function_name": null, + "line": 588, + "path": "src/lib/utils.ts", + "proposed_snippet": null, + "ranking_priority": 0.0, + "rationale": "uninventoried sink path.open reached from parameter", + "reachability_conditions": [], + "reachability_evidence": [], + "reachability_status": "unknown", + "rung": "suspicion", + "severity": "medium", + "status": "enabled", + "tags": [ + "overlay-originated" + ], + "title": "Overlay coverage: path.open" + }, + { + "confidence": "medium", + "evidence_level": "static_corroboration", + "finding_id": "overlay-coverage:src/lib/utils.ts:604:path.open", + "function_name": null, + "line": 604, + "path": "src/lib/utils.ts", + "proposed_snippet": null, + "ranking_priority": 0.0, + "rationale": "uninventoried sink path.open reached from parameter", + "reachability_conditions": [], + "reachability_evidence": [], + "reachability_status": "unknown", + "rung": "suspicion", + "severity": "medium", + "status": "enabled", + "tags": [ + "overlay-originated" + ], + "title": "Overlay coverage: path.open" + }, + { + "confidence": "medium", + "evidence_level": "suspicion", + "finding_id": "model:access_control:src/lib/mcp-auth-config.ts:122:deleteConfigFile", + "function_name": "deleteConfigFile", + "line": 122, + "path": "src/lib/mcp-auth-config.ts", + "proposed_snippet": null, + "ranking_priority": 0.0, + "rationale": "deleteConfigFile line 122: obligated operation with no discharging guard, and no guarded sibling", + "reachability_conditions": [], + "reachability_evidence": [], + "reachability_status": "unknown", + "rung": "model_corroborated", + "severity": "medium", + "status": "enabled", + "tags": [ + "family:access_control", + "rung:model_corroborated" + ], + "title": "access_control (model_corroborated)" + }, + { + "confidence": "low", + "evidence_level": "suspicion", + "finding_id": "tool-hunter:src/lib/coordination.ts:227", + "function_name": null, + "line": 227, + "path": "src/lib/coordination.ts", + "proposed_snippet": null, + "ranking_priority": 14.4621, + "rationale": "coordinateAuth calls setupOAuthCallbackServerWithLongPoll({...}) using callbackPort and serverUrlHash derived from CLI input. This wires the user-controlled port into the local HTTP listener that receives the OAuth code, connecting the CLI entry point to the callback trust boundary.", + "reachability_conditions": [], + "reachability_evidence": [], + "reachability_status": "unknown", + "rung": "suspicion", + "severity": "medium", + "status": "enabled", + "tags": [ + "tool-hunter", + "family:oauth-callback-csrf" + ], + "title": "Auth coordinator starts callback server with CLI-provided port/host" + }, + { + "confidence": "low", + "evidence_level": "suspicion", + "finding_id": "tool-hunter:src/lib/coordination.ts:63", + "function_name": null, + "line": 63, + "path": "src/lib/coordination.ts", + "proposed_snippet": null, + "ranking_priority": 14.4621, + "rationale": "isLockValid reads lockData.port from the on-disk lockfile (written by createLockfile) and interpolates it into fetch(`http://127.0.0.1:${lockData.port}/wait-for-auth?poll=false`). The lockfile is read from the config dir via readJsonFile with only type checks (pid/port/timestamp are numbers) in mcp-auth-config.ts checkLockfile. A tampered lockfile could redirect the probe to an arbitrary local port; impact is limited to localhost but the value is attacker-influenced if the config dir is writable.", + "reachability_conditions": [], + "reachability_evidence": [], + "reachability_status": "unknown", + "rung": "suspicion", + "severity": "medium", + "status": "enabled", + "tags": [ + "tool-hunter", + "family:untrusted-config-input" + ], + "title": "Lockfile port used to build localhost fetch URL without validation" + }, + { + "confidence": "low", + "evidence_level": "suspicion", + "finding_id": "tool-hunter:src/lib/coordination.ts:91", + "function_name": null, + "line": 91, + "path": "src/lib/coordination.ts", + "proposed_snippet": null, + "ranking_priority": 14.4621, + "rationale": "waitForAuthentication(port) builds `http://127.0.0.1:${port}/wait-for-auth` and loops `while (true)` with no maximum attempt count, retrying every 1-2s. Combined with the lockfile-derived port, this is an unbounded polling loop that never terminates on persistent 202 responses, a potential hang/DoS in the auth coordination path.", + "reachability_conditions": [], + "reachability_evidence": [], + "reachability_status": "unknown", + "rung": "suspicion", + "severity": "medium", + "status": "enabled", + "tags": [ + "tool-hunter", + "family:unbounded-loop" + ], + "title": "waitForAuthentication polls attacker-influenced port in unbounded loop" + }, + { + "confidence": "low", + "evidence_level": "suspicion", + "finding_id": "tool-hunter:src/lib/mcp-auth-config.ts:130", + "function_name": null, + "line": 130, + "path": "src/lib/mcp-auth-config.ts", + "proposed_snippet": null, + "ranking_priority": 19.8233, + "rationale": "readJsonFile does JSON.parse(content) then schema.parseAsync(...). The schema is passed as `any` and callers (e.g. checkLockfile) supply ad-hoc parseAsync validators. Deserialization of on-disk config (tokens.json, client_info.json, lock.json) is a trust boundary; malformed or tampered files are parsed before validation, and errors are swallowed returning undefined.", + "reachability_conditions": [], + "reachability_evidence": [], + "reachability_status": "unknown", + "rung": "suspicion", + "severity": "medium", + "status": "enabled", + "tags": [ + "tool-hunter", + "family:deserialization" + ], + "title": "readJsonFile parses JSON from disk with caller-supplied schema" + }, + { + "confidence": "low", + "evidence_level": "suspicion", + "finding_id": "tool-hunter:src/lib/mcp-auth-config.ts:96", + "function_name": null, + "line": 96, + "path": "src/lib/mcp-auth-config.ts", + "proposed_snippet": null, + "ranking_priority": 19.8233, + "rationale": "getConfigFilePath joins configDir with `${serverUrlHash}_${filename}`. serverUrlHash is produced by getServerUrlHash (utils.ts:704) from the user-supplied server URL. If the hash is not strictly hex/limited charset, path traversal characters could influence the resulting path. The filename argument is internal, but the hash component flows from CLI input.", + "reachability_conditions": [], + "reachability_evidence": [], + "reachability_status": "unknown", + "rung": "suspicion", + "severity": "medium", + "status": "enabled", + "tags": [ + "tool-hunter", + "family:path-construction" + ], + "title": "Config file path built from serverUrlHash without sanitization" + }, + { + "confidence": "low", + "evidence_level": "suspicion", + "finding_id": "tool-hunter:src/lib/node-oauth-client-provider.ts:175", + "function_name": null, + "line": 175, + "path": "src/lib/node-oauth-client-provider.ts", + "proposed_snippet": null, + "ranking_priority": 14.3531, + "rationale": "redirectToAuthorization calls open(sanitizeUrl(authorizationUrl.toString())). sanitizeUrl (utils.ts:708) is the guard against dangerous schemes (javascript:, file:, data:) and shell metacharacters, as exercised by utils.test.ts. This is the primary sink where an attacker-controlled authorization URL could reach the OS opener; correctness depends entirely on sanitizeUrl's allowlist.", + "reachability_conditions": [], + "reachability_evidence": [], + "reachability_status": "unknown", + "rung": "suspicion", + "severity": "medium", + "status": "enabled", + "tags": [ + "tool-hunter", + "family:url-scheme-injection" + ], + "title": "Authorization URL passed to open() after sanitizeUrl" + }, + { + "confidence": "low", + "evidence_level": "suspicion", + "finding_id": "tool-hunter:src/lib/utils.ts:364", + "function_name": null, + "line": 364, + "path": "src/lib/utils.ts", + "proposed_snippet": null, + "ranking_priority": 22.9898, + "rationale": "setupOAuthCallbackServerWithLongPoll creates an express app serving /wait-for-auth and options.path, storing the received code in a module-level variable and resolving waitForAuthCode. The callback server is the network entry point for the OAuth redirect; its host/port come from CLI options (host, callbackPort) and it lacks visible state/origin validation on the code endpoint.", + "reachability_conditions": [], + "reachability_evidence": [], + "reachability_status": "unknown", + "rung": "suspicion", + "severity": "medium", + "status": "enabled", + "tags": [ + "tool-hunter", + "family:oauth-callback-csrf" + ], + "title": "OAuth callback server binds and serves auth code over HTTP" + }, + { + "confidence": "low", + "evidence_level": "suspicion", + "finding_id": "tool-hunter:src/lib/utils.ts:375", + "function_name": null, + "line": 375, + "path": "src/lib/utils.ts", + "proposed_snippet": null, + "ranking_priority": 22.9898, + "rationale": "app.get('/wait-for-auth', ...) responds based on the module-level authCode variable and req.query.poll. Any local process (or browser page via localhost) can poll this endpoint to learn whether authentication completed and trigger the long-poll resolution path. There is no origin/host check or token on the endpoint.", + "reachability_conditions": [], + "reachability_evidence": [], + "reachability_status": "unknown", + "rung": "suspicion", + "severity": "medium", + "status": "enabled", + "tags": [ + "tool-hunter", + "family:local-endpoint-exposure" + ], + "title": "Unauthenticated local /wait-for-auth endpoint leaks auth completion state" + }, + { + "confidence": "low", + "evidence_level": "suspicion", + "finding_id": "tool-hunter:src/lib/utils.ts:415", + "function_name": null, + "line": 415, + "path": "src/lib/utils.ts", + "proposed_snippet": null, + "ranking_priority": 22.9898, + "rationale": "setupOAuthCallbackServerWithLongPoll registers app.get(options.path, ...) which reads req.query.code and assigns it to authCode (line 422) and resolves the auth promise (line 448). No comparison of req.query.state against the provider's state() value is visible in this handler, so an attacker who can reach the local callback server could inject an authorization code (CSRF on the OAuth callback). The provider does generate state via randomUUID (node-oauth-client-provider.ts:41), but the callback handler here does not appear to verify it.", + "reachability_conditions": [], + "reachability_evidence": [], + "reachability_status": "unknown", + "rung": "suspicion", + "severity": "medium", + "status": "enabled", + "tags": [ + "tool-hunter", + "family:oauth-callback-csrf" + ], + "title": "OAuth callback endpoint accepts code without state validation" + }, + { + "confidence": "low", + "evidence_level": "suspicion", + "finding_id": "tool-hunter:src/lib/utils.ts:426", + "function_name": null, + "line": 426, + "path": "src/lib/utils.ts", + "proposed_snippet": null, + "ranking_priority": 22.9898, + "rationale": "The callback handler responds with res.send(`...`) (line 426). If any request-derived value (e.g. req.query.code or error params) is interpolated into this HTML response without escaping, it is a reflected-content sink in the local callback page. The exact template body was not fully read, so this is a suspicion to verify.", + "reachability_conditions": [], + "reachability_evidence": [], + "reachability_status": "unknown", + "rung": "suspicion", + "severity": "medium", + "status": "enabled", + "tags": [ + "tool-hunter", + "family:reflected-content" + ], + "title": "Callback response rendered via res.send with interpolated content" + }, + { + "confidence": "low", + "evidence_level": "suspicion", + "finding_id": "tool-hunter:src/lib/utils.ts:530", + "function_name": null, + "line": 530, + "path": "src/lib/utils.ts", + "proposed_snippet": null, + "ranking_priority": 22.9898, + "rationale": "parseCommandLineArgs(args, usage) is invoked with process.argv.slice(2) in both proxy.ts:144 and client.ts:161, producing serverUrl, callbackPort, headers, host, and static OAuth metadata that flow into the OAuth provider and callback server. This is the entry point for all user-controlled configuration (server URL, host, headers) that later reaches sanitizeUrl, getServerUrlHash, and the callback server bind.", + "reachability_conditions": [], + "reachability_evidence": [], + "reachability_status": "unknown", + "rung": "suspicion", + "severity": "medium", + "status": "enabled", + "tags": [ + "tool-hunter", + "family:cli-input-entry" + ], + "title": "parseCommandLineArgs consumes raw process.argv" + }, + { + "confidence": "low", + "evidence_level": "suspicion", + "finding_id": "tool-hunter:src/lib/utils.ts:60", + "function_name": null, + "line": 60, + "path": "src/lib/utils.ts", + "proposed_snippet": null, + "ranking_priority": 22.9898, + "rationale": "debugLog builds a log path from global.currentServerUrlHash and appends JSON.stringify(arg) of arbitrary message/params (including remote MCP messages logged in mcpProxy). Remote-controlled message content is serialized into a file under the config dir; combined with the hash-derived filename this is a filesystem write of untrusted data.", + "reachability_conditions": [], + "reachability_evidence": [], + "reachability_status": "unknown", + "rung": "suspicion", + "severity": "medium", + "status": "enabled", + "tags": [ + "tool-hunter", + "family:untrusted-log-write" + ], + "title": "debugLog writes attacker-influenced data to disk log file" + }, + { + "confidence": "low", + "evidence_level": "suspicion", + "finding_id": "tool-hunter:src/lib/utils.ts:704", + "function_name": null, + "line": 704, + "path": "src/lib/utils.ts", + "proposed_snippet": null, + "ranking_priority": 22.9898, + "rationale": "getServerUrlHash(serverUrl) is called with the CLI-provided server URL (proxy.ts:43, client.ts:44, node-oauth-client-provider.ts:35) and its result is used as a filename prefix in getConfigFilePath and as the debug log filename. If the hash is not a fixed-length hex digest, the user-controlled URL could influence on-disk paths.", + "reachability_conditions": [], + "reachability_evidence": [], + "reachability_status": "unknown", + "rung": "suspicion", + "severity": "medium", + "status": "enabled", + "tags": [ + "tool-hunter", + "family:path-construction" + ], + "title": "getServerUrlHash derives filesystem key from user URL" + }, + { + "confidence": "low", + "evidence_level": "suspicion", + "finding_id": "tool-hunter:src/lib/utils.ts:708", + "function_name": null, + "line": 708, + "path": "src/lib/utils.ts", + "proposed_snippet": null, + "ranking_priority": 22.9898, + "rationale": "sanitizeUrl is the security-critical function protecting open() in node-oauth-client-provider.ts. The test file (utils.test.ts) asserts rejection of javascript:, file:, data:, and $(...) host/path payloads, indicating this is the intended defense. Any gap in its protocol/host allowlist directly enables command/URL injection via the browser opener.", + "reachability_conditions": [], + "reachability_evidence": [], + "reachability_status": "unknown", + "rung": "suspicion", + "severity": "medium", + "status": "enabled", + "tags": [ + "tool-hunter", + "family:url-scheme-injection" + ], + "title": "sanitizeUrl is the sole gate before open()" + } + ] +}