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:634error_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 widepackages/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 unboundedexec_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 blacklistpackages/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.)

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