Skip to content

feat: Rewrite secret placeholders in L7 request bodies #459

@drew

Description

@drew

Problem Statement

The L7 proxy's SecretResolver only rewrites openshell:resolve:env:* placeholders in HTTP headers. Some API clients (notably the Slack Node SDK) send auth tokens in the POST body (e.g., token=xoxb-... as form-encoded data). When the sandbox child process uses a generic provider to obfuscate secrets, the placeholder string leaks verbatim to the upstream API, causing invalid_auth errors.

This was discovered with OpenClaw (Slack bot) crashing on startup with An API error occurred: invalid_auth from POST /api/auth.test to slack.com:443, despite having L7 REST with TLS terminate active and the proxy correctly intercepting requests.

Proposed Design

Extend the existing SecretResolver to also scan and rewrite request bodies when:

  1. A resolver is present (providers are configured)
  2. The body size is within a safety limit (1 MiB) to prevent unbounded memory use
  3. The body is valid UTF-8 (binary bodies are passed through)

Changes

crates/openshell-sandbox/src/secrets.rs

  • Add MAX_REWRITE_BODY_BYTES constant (1 MiB)
  • Add SecretResolver::rewrite_body(&self, body: &[u8]) -> Option<Vec<u8>> — scans body bytes for placeholder substrings and replaces them with real secrets. Returns None if no placeholders found (zero-copy fast path).
  • Add rewrite_content_length(headers: &[u8], new_length: u64) -> Vec<u8> — updates Content-Length header when body size changes after rewriting.

crates/openshell-sandbox/src/l7/rest.rs

  • Modify relay_http_request_with_resolver() to buffer the body (for Content-Length <= 1 MiB), call rewrite_body(), update Content-Length if the body size changed, then forward. Bodies exceeding the limit or chunked bodies that exceed the limit during buffering fall back to the existing streaming behavior.

crates/openshell-sandbox/src/proxy.rs

  • Modify rewrite_forward_request() to apply body rewriting to overflow body bytes in the forward proxy path.

Edge cases

  • Bodies > 1 MiB: streamed without rewriting (existing behavior)
  • Non-UTF-8 bodies: passed through unmodified
  • No placeholders in body: no allocation, unchanged
  • Content-Length adjustment: header updated to match rewritten body size
  • Chunked transfer: de-chunk, rewrite, send as Content-Length (within size limit)

Alternatives Considered

  • Client-side fix: Configure the Slack SDK to send tokens via Authorization header instead of body. This works for Slack specifically but doesn't solve the general problem for other APIs that embed tokens in request bodies.
  • Body rewriting with no size limit: Rejected due to memory exhaustion risk on large file uploads.

Agent Investigation

Root cause traced through the codebase:

  • SecretResolver::rewrite_header_value() at crates/openshell-sandbox/src/secrets.rs:37-48 handles only header values (exact match or <scheme> <placeholder> pattern).
  • rewrite_http_header_block() at secrets.rs:55-86 iterates header lines only; line 84 passes the body through as-is.
  • relay_http_request_with_resolver() at crates/openshell-sandbox/src/l7/rest.rs:129-172 streams the body directly via relay_fixed/relay_chunked without inspection.
  • The existing test relay_request_without_resolver_leaks_placeholders at rest.rs:1096-1170 explicitly documents that placeholders leak when no resolver is present, but there is no corresponding test for body content.

Metadata

Metadata

Assignees

No one assigned

    Labels

    area:sandboxSandbox runtime and isolation workarea:supervisorProxy and routing-path worktopic:l7Application-layer policy and inspection work

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions