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
pnpm update minimatch (≥10.2.3) — fixes H1+H2, the only model-input-triggerable high
- Fix M1 (one-line HTML escape) + M2 (Windows URL handling)
- Fix M3/M4 (memory caps)
- 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.)
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 atpackages/core/src/tools/tools/filesystem/lib.ts:336,lib.ts:340(search_filestool)search_filespattern/excludePatternsare model-controlled and passed straight tominimatch(relativePath, pattern). Patterns like**/**/**/xor nested extglobs can trigger exponential backtracking and hang the agent process (local DoS).minimatchto>=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){a{0..N}}patterns (same model-controlled surface as H1). picomatch only used with static**/SKILL.mdpatterns inskills.ts:307— not directly model-triggerable.H3. undici outdated (11 paths, multiple high advisories)
undici@6.23.0(packages/core/package.json, used for webfetchProxyAgent); transitive@ai-sdk/provider-utils > undici@5.29.0(11 paths) andjsdom > undici@7.22.0globalThis.fetch; undici only forProxyAgentatwebfetch.ts:312), but CRLF/smuggling becomes reachable when proxying remote MCP servers.undici→>=6.27.0; track@ai-sdk/provider-utilsandjsdomupgrades.Medium
M1. Reflected XSS in OAuth callback page (localhost)
packages/core/src/tools/router/mcp/oauth.ts:634—error_description/errorfrom the OAuth server is concatenated into HTML without escaping (res.end('<html>...<p>' + message + '</p>...'))<script>in the callback URL; when the user opens127.0.0.1:<port>/callback, the script runs on the localhost origin and can hit other local ports / read same-origin storage.message, or respond withtext/plain.M2. Command injection on Windows:
cmd /c startfor OAuth URLpackages/core/src/tools/router/mcp/oauth.ts:274(win32 branch{ command: 'cmd', args: ['/c', 'start', '', url] })cmd.exe /cparses 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.)rundll32 url.dll,FileProtocolHandlerwith quote validation, or PowerShellStart-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'soutputis never freedexec_commandhas no defaultexecution_timeout_ms(onlyshell_commandsets one).yesorcat /dev/urandom | base64can exhaust memory;write_stdinkeeps appending.outputfor completed sessions; default timeout forexec_command.M4. No file size limit on reads
packages/core/src/tools/tools/filesystem/lib.ts:130(fs.readFilefull read),lib.ts:158(applyFileEdits);read_media_file.ts:47(full base64 read)sdk_tools.ts:63guardToolResultOutput, 20k chars) happens only after the whole file is in memory. Large files in the workspace → memory DoS.stat-based size cap (e.g. 50MB) or streaming truncation; same forread_media_file.M5. webfetch SSRF: DNS rebinding TOCTOU (known limitation)
packages/core/src/tools/tools/webfetch.ts:138(assertPublicHostresolves DNS once, then fetch)MEMO_WEBFETCH_BLOCK_PRIVATE_NET=0disables the whole guard (webfetch.ts:99). Redirect hops are re-checked (webfetch.ts:204). Baseline protection is solid; this is a known residual gap.Low
packages/core/src/features/history/history_sink.ts:27:appendFilewithout mode → 0o644;~/.memo/sessions/*.jsonlcontains full conversations/tool args/command output (may include secrets). OAuth creds use 0o600 (oauth.ts:196). Fix:{ mode: 0o600 }.exec_runtime.ts:259passes fullprocess.env(API keys, cloud creds) to children;exec_runtime.ts:245allows../escapes from workspace. Inconsistent with the sandboxed read/write tools; document, or validate allowed roots / filter env.memo initapproval whitelist bypass via*** Move to:—packages/tui/src/shared/lib/initApproval.ts:4only matches*** Add/Update File:; a prompt-injected model can*** Update File:+*** Move to:another path inside the workspace. Fix: parse and validateMove totargets.packages/core/src/tools/tools/command_guard.tscovers only a small set (rm -rfkey paths, mkfs/dd, ...).find / -delete, fork bombs,tar --to-command, nestedsudo bash -c '...'all bypass it. It's a speed bump, not a security boundary — document that the approval gate is the real boundary.config.ts:201/175(self-harm only);get_memory(get_memory.ts:22) always returns the entire~/.memo/Agents.mdto any agent regardless ofmemory_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 inshell.ts:21-27realpathsymlink checks (filesystem/lib.ts:97),canonicalWritePath(apply_patch.ts:565), atomic writes withwxcrypto.randomUUIDfor session/agent ids;site/is a static export, no server codeSuggested priority
pnpm update minimatch(≥10.2.3) — fixes H1+H2, the only model-input-triggerable high(This issue was generated from a subagent security audit — details may be worth verifying against the current tree before fixing.)