Skip to content

[Bug]: ctx_fetch_and_index silently ignores HTTP_PROXY/HTTPS_PROXY — fetch subprocess strips all proxy env vars #1039

Description

@DenisBalan

Platform

Pi

Description

ctx_fetch_and_index bypasses any configured outbound proxy entirely. In corporate/audited environments where all egress must route through a proxy (mitmproxy, Zscaler, Squid),
context-mode silently performs direct egress, bypassing the security control. This was discovered while the host shell was configured with HTTP_PROXY=http://127.0.0.1:9080:
the MCP tool fetched URLs that never appeared in the proxy's flow log, and would fail outright in networks with no direct route to the internet.

The cause is deliberate in code: the subprocess script generated for each fetch deletes every proxy variable before fetch() runs.

Context-mode version

1.0.169 (installed via pi MCP; ctx upgrade reports already on latest)

Debug script output

Not relevant — this is a behavioral defect, not a crash. Environment at repro time:

  HTTP_PROXY=http://127.0.0.1:9080                                                                                                                                                        
  http_proxy=http://127.0.0.1:9080                                                                                                                                                        
  HTTPS_PROXY=http://127.0.0.1:9080                                                                                                                                                       
  https_proxy=http://127.0.0.1:9080                                                                                                                                                       

Port 9080 is a running proxy instance. The URL fetched by the agent never appeared in proxy's traffic log; the connection was made direct.

Exact prompt that triggered bug

Any ctx_fetch_and_index call when proxy env vars are set. E.g.:

  ctx_fetch_and_index(url: "https://learn.microsoft.com/en-us/cli/azure/ad/app/credential")                                                                                               

Full error output

No error — that's the problem. The fetch silently succeeds over a direct connection, invisible to the proxy. There is no warning in the tool response that the proxy was bypassed.

Steps to reproduce

Structural fact: in src/server.ts (~line 2876, the fetch-subprocess script builder used by ctx_fetch_and_index):

  // Strip proxy env vars from this subprocess only. A configured outbound                                                                                                                
  // proxy (HTTP_PROXY / HTTPS_PROXY / ALL_PROXY) would route fetch through                                                                                                               
  // an arbitrary target — DNS resolution happens at the proxy and the                                                                                                                    
  // in-subprocess DNS rebinding guard never sees the rebound IP. The                                                                                                                     
  // sandbox fetch path has no legitimate need for an upstream proxy.                                                                                                                     
  delete process.env.HTTP_PROXY;                                                                                                                                                          
  delete process.env.HTTPS_PROXY;                                                                                                                                                         
  delete process.env.ALL_PROXY;                                                                                                                                                           
  delete process.env.http_proxy;                                                                                                                                                          
  delete process.env.https_proxy;                                                                                                                                                         
  delete process.env.all_proxy;                                                                                                                                                           
  delete process.env.npm_config_proxy;                                                                                                                                                    
  delete process.env.npm_config_https_proxy;                                                                                                                                              

Node's global fetch (undici EnvHttpProxyAgent) honors HTTP_PROXY/HTTPS_PROXY automatically; deleting them forces a direct connection. The comment's rationale (DNS-rebinding guard must
see the resolved IP) is real but the fix is wrong-shaped: it trades an SSRF edge case for universal proxy bypass with zero opt-out.

Self-contained repro against the real MCP server over stdio (isolated HOME, proxy pointed at a closed port so proxy-respected ⇒ connection refused, proxy-bypassed ⇒ fetch succeeds):

  git clone https://github.com/mksglu/context-mode                                                                                                                                        
  cd context-mode                                                                                                                                                                         
  npm install                                                                                                                                                                             
  node --input-type=module <<'EOF'                                                                                                                                                        
  import { spawn } from "node:child_process";                                                                                                                                             
  import { mkdtempSync } from "node:fs";                                                                                                                                                  
  import { tmpdir } from "node:os";                                                                                                                                                       
  import { join } from "node:path";                                                                                                                                                       
                                                                                                                                                                                          
  const home = mkdtempSync(join(tmpdir(), "cm-proxy-home-"));                                                                                                                             
  const proj = mkdtempSync(join(tmpdir(), "cm-proxy-proj-"));                                                                                                                             
  const srv = spawn("node", ["start.mjs"], {                                                                                                                                              
    env: { ...process.env, HOME: home, XDG_DATA_HOME: home,                                                                                                                               
      CLAUDE_SESSION_ID: "cm-proxy-repro", CLAUDE_PROJECT_DIR: proj,                                                                                                                      
      CONTEXT_MODE_PROJECT_DIR: proj, PWD: proj,                                                                                                                                          
      HTTP_PROXY: "http://127.0.0.1:1", http_proxy: "http://127.0.0.1:1",                                                                                                                 
      HTTPS_PROXY: "http://127.0.0.1:1", https_proxy: "http://127.0.0.1:1" },                                                                                                             
    stdio: ["pipe", "pipe", "inherit"],                                                                                                                                                   
  });                                                                                                                                                                                     
  let buf = ""; const pending = new Map(); let id = 1;                                                                                                                                    
  srv.stdout.on("data", d => {                                                                                                                                                            
    buf += d; let i;                                                                                                                                                                      
    while ((i = buf.indexOf("\n")) >= 0) {                                                                                                                                                
      const line = buf.slice(0, i).trim(); buf = buf.slice(i + 1);                                                                                                                        
      if (!line) continue;                                                                                                                                                                
      try { const m = JSON.parse(line); if (m.id && pending.has(m.id)) { pending.get(m.id)(m); pending.delete(m.id); } } catch {}                                                         
    }                                                                                                                                                                                     
  });                                                                                                                                                                                     
  const rpc = (method, params) => new Promise(res => { const i = id++; pending.set(i, res); srv.stdin.write(JSON.stringify({ jsonrpc: "2.0", id: i, method, params }) + "\n"); });        
  const sleep = ms => new Promise(r => setTimeout(r, ms));                                                                                                                                
  await rpc("initialize", { protocolVersion: "2024-11-05", capabilities: {}, clientInfo: { name: "repro", version: "0" } });                                                              
  srv.stdin.write(JSON.stringify({ jsonrpc: "2.0", method: "notifications/initialized" }) + "\n");                                                                                        
  await sleep(500);                                                                                                                                                                       
  const r = await rpc("tools/call", { name: "ctx_fetch_and_index",                                                                                                                        
    arguments: { url: "https://example.com/", source: "proxy-repro", ttl: 0 } });                                                                                                         
  console.log(r?.result?.content?.map(c => c.text).join("\n").slice(0, 400));                                                                                                             
  srv.kill(); process.exit(0);                                                                                                                                                            
  EOF                                                                                                                                                                                     

Expected: fetch fails with ECONNREFUSED 127.0.0.1:1 (proxy respected — undici would CONNECT to the proxy).
Actual (observed on 1.0.169): Fetched and indexed 2 sections ... proxy-repro::https://example.com/ — fetch succeeded over a direct connection despite the only configured egress
route being a closed port.

Observational variant (no repro script needed): run the MCP host with HTTP_PROXY=http://127.0.0.1:9080 pointing at a mitmproxy instance, call ctx_fetch_and_index on any URL, and note
the flow never appears in the proxy UI while the tool reports success.

What I tried to fix it

Read the fetch pipeline end to end: ctx_fetch_and_index → fetchAndIndex → SSRF pre-flight ssrfGuard (resolves + classifies the hostname in the parent) → subprocess built by the script
builder above runs fetch() with proxy vars deleted. The parent-side DNS check already happens before the subprocess exists, so the proxy-stripping rationale only covers the narrow
window of rebinding between check and connect. Two directions that preserve the SSRF guarantee:

  1. Opt-in proxy (minimal): gate the stripping on an env var, e.g. CTX_FETCH_PROXY=1 skips the deletes. Users behind mandatory proxies set it and accept the documented rebinding window
    (which is already present in spirit — NO_PROXY/system proxies are currently silently ignored, not safely handled).
  2. Proxy-aware guard: when a proxy is configured, classify the proxy's address against the same classifyIp policy (blocking private/link-local proxies unless CTX_FETCH_STRICT is off)
    and keep the vars. DNS now resolves at the proxy, so also resolve the target in the parent and treat any block verdict as fatal — which the pre-flight already does.

Either way: when proxy env vars are present and get stripped, say so in the tool response (one line), so users auditing egress aren't blindsided.

Operating System

Linux (Ubuntu, x86_64)

JS Runtime

Node v24.16.0

Pre-submission checklist

JS Runtime

v24.16.0

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions