mirror of
https://github.com/norandom/OpenUltraSAST.git
synced 2026-10-20 13:39:27 +00:00
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 <[email protected]>
612 lines
24 KiB
JSON
612 lines
24 KiB
JSON
{
|
|
"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()"
|
|
}
|
|
]
|
|
}
|