Skip to content

Security audit: dependency ReDoS, OAuth XSS/cmd injection, unbounded memory, and more #225

Description

@minorcell

Security audit of the repo on branch refactor/ai-sdk (current working tree). This is a local CLI agent tool, so the main threat model is: prompt-injected models abusing tools / malicious MCP servers / malicious web content. No remotely-exploitable criticals found. Findings ordered by severity.

High

H1. minimatch@10.2.0 ReDoS (model-controlled input)

  • packages/core/package.json (minimatch: ^10.0.1); used at packages/core/src/tools/tools/filesystem/lib.ts:336, lib.ts:340 (search_files tool)
  • GHSA-3ppc-4f35-3m26 / GHSA-7r86-cg39-jmmj / GHSA-23c5-xmqv-rm74 — fixed in 10.2.3
  • search_files pattern/excludePatterns are model-controlled and passed straight to minimatch(relativePath, pattern). Patterns like **/**/**/x or nested extglobs can trigger exponential backtracking and hang the agent process (local DoS).
  • Fix: upgrade minimatch to >=10.2.3; consider limiting pattern length/complexity (e.g. reject >3 **).

H2. Transitive ReDoS: brace-expansion / picomatch

  • pnpm-lock.yaml: minimatch@10.2.0 > brace-expansion@5.0.2 (GHSA-f886-m6hf-6m8v, GHSA-3qgp-pqcv-9wcr; fixed in 5.0.8); fast-glob@3.3.3 > micromatch@4.0.8 > picomatch@2.3.1 (GHSA-c2c7-rcm5-vvqj; fixed in 2.3.2)
  • brace-expansion triggers via minimatch {a{0..N}} patterns (same model-controlled surface as H1). picomatch only used with static **/SKILL.md patterns in skills.ts:307 — not directly model-triggerable.
  • Fix: upgrading minimatch to 10.2.3 pulls brace-expansion fix; upgrade fast-glob/micromatch for picomatch.

H3. undici outdated (11 paths, multiple high advisories)

  • Direct dep undici@6.23.0 (packages/core/package.json, used for webfetch ProxyAgent); transitive @ai-sdk/provider-utils > undici@5.29.0 (11 paths) and jsdom > undici@7.22.0
  • Advisories: WebSocket / HTTP smuggling / CRLF injection. Reachability is low (webfetch uses native globalThis.fetch; undici only for ProxyAgent at webfetch.ts:312), but CRLF/smuggling becomes reachable when proxying remote MCP servers.
  • Fix: direct undici → >=6.27.0; track @ai-sdk/provider-utils and jsdom upgrades.

Medium

M1. Reflected XSS in OAuth callback page (localhost)

  • packages/core/src/tools/router/mcp/oauth.ts:634 — error_description/error from the OAuth server is concatenated into HTML without escaping (res.end('<html>...<p>' + message + '</p>...'))
  • A malicious MCP server can put <script> in the callback URL; when the user opens 127.0.0.1:<port>/callback, the script runs on the localhost origin and can hit other local ports / read same-origin storage.
  • Fix: HTML-escape message, or respond with text/plain.

M2. Command injection on Windows: cmd /c start for OAuth URL

  • packages/core/src/tools/router/mcp/oauth.ts:274 (win32 branch { command: 'cmd', args: ['/c', 'start', '', url] })
  • The authorization URL comes from the MCP server's OAuth discovery endpoint (attacker-controlled). cmd.exe /c parses the whole command line — URL metacharacters (&, |, ") can act as command separators; a " in the URL breaks spawn's quoting → arbitrary command execution. (macOS/Linux argv passing is safe.)
  • Fix: use rundll32 url.dll,FileProtocolHandler with quote validation, or PowerShell Start-Process; reject URLs containing "/|/&.

M3. Unbounded exec session output accumulation (memory DoS)

  • packages/core/src/tools/tools/exec_runtime.ts:278 (session.output += ...); sessions capped at 64 (lines 20, 151) but each session's output is never freed
  • exec_command has no default execution_timeout_ms (only shell_command sets one). yes or cat /dev/urandom | base64 can exhaust memory; write_stdin keeps appending.
  • Fix: hard cap output (e.g. 16MB) and kill on exceed; free output for completed sessions; default timeout for exec_command.

M4. No file size limit on reads

  • packages/core/src/tools/tools/filesystem/lib.ts:130 (fs.readFile full read), lib.ts:158 (applyFileEdits); read_media_file.ts:47 (full base64 read)
  • Truncation (sdk_tools.ts:63 guardToolResultOutput, 20k chars) happens only after the whole file is in memory. Large files in the workspace → memory DoS.
  • Fix: stat-based size cap (e.g. 50MB) or streaming truncation; same for read_media_file.

M5. webfetch SSRF: DNS rebinding TOCTOU (known limitation)

  • packages/core/src/tools/tools/webfetch.ts:138 (assertPublicHost resolves DNS once, then fetch)
  • Domain can re-resolve to internal IP between check and request; MEMO_WEBFETCH_BLOCK_PRIVATE_NET=0 disables the whole guard (webfetch.ts:99). Redirect hops are re-checked (webfetch.ts:204). Baseline protection is solid; this is a known residual gap.
  • Fix (optional): re-validate IP at the connection layer / pin the resolved IP.

Low

  • L1. Session files default permissions too wide — packages/core/src/features/history/history_sink.ts:27: appendFile without mode → 0o644; ~/.memo/sessions/*.jsonl contains full conversations/tool args/command output (may include secrets). OAuth creds use 0o600 (oauth.ts:196). Fix: { mode: 0o600 }.
  • L2. exec env/workdir unbounded — exec_runtime.ts:259 passes full process.env (API keys, cloud creds) to children; exec_runtime.ts:245 allows ../ escapes from workspace. Inconsistent with the sandboxed read/write tools; document, or validate allowed roots / filter env.
  • L3. memo init approval whitelist bypass via *** Move to: — packages/tui/src/shared/lib/initApproval.ts:4 only matches *** Add/Update File:; a prompt-injected model can *** Update File: + *** Move to: another path inside the workspace. Fix: parse and validate Move to targets.
  • L4. Dangerous-command guard is a bypassable blacklist — packages/core/src/tools/tools/command_guard.ts covers only a small set (rm -rf key paths, mkfs/dd, ...). find / -delete, fork bombs, tar --to-command, nested sudo bash -c '...' all bypass it. It's a speed bump, not a security boundary — document that the approval gate is the real boundary.
  • L5. Misc — TOML section names unescaped in config.ts:201/175 (self-harm only); get_memory (get_memory.ts:22) always returns the entire ~/.memo/Agents.md to any agent regardless of memory_id (sensitive context leak surface); new Function('specifier', ...) in oauth.ts:152 (hardcoded 'keytar', cosmetic); sandbox_permissions: 'require_escalated' is a no-op (exec_command.ts:31 / exec_runtime.ts don't consume it).

Verified safe

  • spawn(file, args, { shell: false }) — no shell injection; argv quoting correct in shell.ts:21-27
  • FS sandbox: realpath symlink checks (filesystem/lib.ts:97), canonicalWritePath (apply_patch.ts:565), atomic writes with wx
  • webfetch SSRF baseline: private/loopback/link-local blocked by default, per-hop redirect validation, robots.txt, body size caps
  • No eval/dynamic exec (except hardcoded keytar import), no unsafe deserialization, no TLS disabled, no install-script hooks in lockfile
  • crypto.randomUUID for session/agent ids; site/ is a static export, no server code

Suggested priority

  1. pnpm update minimatch (≥10.2.3) — fixes H1+H2, the only model-input-triggerable high
  2. Fix M1 (one-line HTML escape) + M2 (Windows URL handling)
  3. Fix M3/M4 (memory caps)
  4. Upgrade undici (H3); tighten L1 file permissions

(This issue was generated from a subagent security audit — details may be worth verifying against the current tree before fixing.)

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    area:coreCore runtime and session statearea:securityApproval, sandbox, and security policyarea:toolsBuilt-in tools and tool runtimearea:tuiTerminal UI and interaction layerneeds-triageIssue needs initial triage

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions