Skip to content

tls-terminate-proxy crashes the process (ERR_INVALID_CHAR) on a non-latin1 response header #490

Description

@onesuper

Summary

@anthropic-ai/sandbox-runtime 0.0.73's tls-terminate-proxy crashes the entire host process when an upstream returns a response header value containing a code point > 0xFF. A common trigger: a Feishu (Lark)
file download whose content-disposition carries a UTF-8 (non-ASCII) filename.

Version

@anthropic-ai/sandbox-runtime 0.0.73 (latest on npm as of this report).

Repro

Sandboxed bash makes an HTTPS request through SRT's egress (terminated-TLS proxy) to an endpoint that returns a response header with a non-latin1 value. Example: a Feishu im/v1/messages/{id}/resources file
download returns:

content-disposition: attachment; filename="中文名.pdf"

Stack

TypeError [ERR_INVALID_CHAR]: Invalid character in header content ["content-disposition"]
    at storeHeader (node:_http_outgoing:628:5)
    at processHeader (node:_http_outgoing:623:3)
    at ServerResponse._storeHeader (node:_http_outgoing:492:11)
    at ServerResponse.writeHead (node:_http_server:439:8)
    at ClientRequest.<anonymous> (.../@anthropic-ai/sandbox-runtime@0.0.73/.../dist/sandbox/tls-terminate-proxy.js:419:13)
    at Object.onceWrapper (node:events:634:26)
    ...

Root cause

dist/sandbox/tls-terminate-proxy.js:419:

res.writeHead(upRes.statusCode ?? 502, stripHopByHop(upRes.headers));

upRes.headers is forwarded verbatim into res.writeHead. Node's HTTP server rejects any header value with a code point > 0xFF (ERR_INVALID_CHAR). The throw happens inside the httpsRequest(..., upRes => {...}) response callback, which fires on a later event-loop tick — after the async forwardUpstream has already returned — so forwardUpstreamGuarded's forwardUpstream(...).catch(...) cannot catch it,
and the upRes.on('error') / upstream.on('error') listeners only handle stream error events, not a synchronous throw. The throw escapes as an uncaughtException and, with no process-level handler in the
host, Node exits the process (exit code 1).

In our deployment SRT runs in-process inside a single-process sidecar daemon, so one such download kills the daemon and takes down every concurrent sandboxed-bash call on that pod until the container restarts
(~30s). Observed 3 restarts in production.

Suggested fix

Sanitize response header values before writeHead — replace code points > 0xFF (e.g. with ?, or RFC 5987-encode filename*), so writeHead never sees a value it rejects. Concretely, wrap line 419:

res.writeHead(upRes.statusCode ?? 502, sanitizeResponseHeaders(stripHopByHop(upRes.headers)));

with

res.writeHead(upRes.statusCode ?? 502, sanitizeResponseHeaders(stripHopByHop(upRes.headers)));

with

function sanitizeResponseHeaders(headers) {
  const out = {};
  for (const key of Object.keys(headers)) {
    const v = headers[key];
    out[key] = Array.isArray(v) ? v.map(sanitizeHeaderValue) : sanitizeHeaderValue(v);
  }
  return out;
}
function sanitizeHeaderValue(v) {
  return typeof v === 'string' ? v.replace(/[^\x00-\xFF]/g, '?') : v;
}

Alternatively, wrap the writeHead in try/catch and write a 502 so the proxy survives and the client gets an explicit error instead of a hang.

Notes

  • HTTP/1.1 headers are technically latin1; a well-behaved origin should use RFC 5987 filename*=UTF-8''… for non-ASCII filenames. But many real servers (Feishu here) emit raw UTF-8 in content-disposition,
    and a proxy that crashes the whole process on one non-conforming upstream header is a robustness bug.
  • Worth checking: the same verbatim-forward pattern may exist anywhere else response headers are written back through the proxy.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions