Skip to content

Bug: allow_encoded_slash sentinel byte written directly to wire in rewrite_forward_request #987

@derekhsu

Description

@derekhsu

Agent Diagnostic

  • Loaded debug-openshell-cluster and openshell-cli skills
  • Traced allow_encoded_slash through OpenShell source:
    • path.rs:98 defines ENCODED_SLASH_SENTINEL = 0x01
    • path.rs:215: allow_encoded_slash=true → %2F → 0x01 sentinel
    • path.rs:289: build_canonical_path reconstructs %2F from 0x01 (in-memory canonical form)
    • proxy.rs:1957: rewrite_forward_request calls output.extend_from_slice(path.as_bytes()) — writes sentinel directly to wire, no re-encoding
  • Found regression test test_rewrite_forward_request_uses_canonical_path_on_the_wire only covers .. path traversal, not encoded slashes
  • Found parse_http_request_accepts_encoded_slash_when_endpoint_opts_in only tests req.target in-memory, not the rewritten wire bytes
  • This is the root cause of openclaw onboard hanging when allow_encoded_slash=true — upstream receives 0x01 (non-printable byte) in request-target and closes connection

Description

When allow_encoded_slash: true is set on an endpoint in the sandbox policy, any HTTP request forwarded through the forward proxy (plain HTTP, not CONNECT tunnel) that contains an encoded slash (%2F) in its request-target will have the sentinel byte 0x01 written directly to the wire instead of being re-encoded to %2F.

This causes the upstream server to receive a malformed request-target containing 0x01, which either closes the connection immediately (ECONNRESET) or waits indefinitely without responding.

This bug was introduced in v0.0.34 by commit c960d48 (fix(sandbox): canonicalize HTTP request-targets before L7 policy evaluation (#878)), which added the ENCODED_SLASH_SENTINEL mechanism but missed the wire-encoding step in rewrite_forward_request.


Reproduction Steps

  1. Set up OpenShell cluster with a sandbox policy that has allow_encoded_slash: true on an HTTP endpoint (e.g., any endpoint that proxies to registry.npmjs.org)
  2. Run openclaw onboard (or any tool that performs npm package requests from a forward-proxy sandbox)
  3. Request hangs indefinitely — no response, no error, connection never completes

Minimal reproduction without OpenClaw:

# From inside the sandbox, via forward proxy
curl -v http://registry.npmjs.org/@anthropic-ai%2Fvertex-sdk
# Hangs — upstream sees malformed request-target with 0x01 byte

Root Cause

In crates/openshell-sandbox/src/l7/path.rs:

  • ENCODED_SLASH_SENTINEL = 0x01 (line 98)
  • canonicalize_request_target: %2F → 0x01 (line 215)
  • build_canonical_path: 0x01 → %2F in the canonical string (line 289)

In crates/openshell-sandbox/src/proxy.rs, rewrite_forward_request (line 1957):

output.extend_from_slice(path.as_bytes()); // ← BUG: 0x01 written as-is, not re-encoded

The path string in memory contains 0x01 bytes (from build_canonical_path producing a string with those bytes). When written to the wire, they are not re-encoded to %2F.


Expected Behavior

When allow_encoded_slash: true, the forward proxy should:

  1. Accept %2F in the incoming request-target
  2. Store it internally as 0x01 sentinel during policy evaluation
  3. When forwarding to upstream, re-encode 0x01 back to %2F in the wire bytes

Fix

In rewrite_forward_request (proxy.rs ~line 1957), before writing path to output, replace 0x01 bytes with %2F:

let rewritten_path: Vec<u8> = path.bytes().flat_map(|b| {
    if b == ENCODED_SLASH_SENTINEL {
        b"%2F".to_vec()
    } else {
        vec![b]
    }
}).collect();
output.extend_from_slice(&rewritten_path);

Or equivalently, use the existing build_canonical_path logic which already handles this — but the current code path bypasses it at the wire-write stage.


Additional Test Gaps

The existing test test_rewrite_forward_request_uses_canonical_path_on_the_wire only covers .. path traversal. A new test is needed:

#[test]
fn test_rewrite_forward_request_reencodes_sentinel_to_percent_encoding() {
    // allow_encoded_slash: true — sentinel 0x01 must be re-encoded to %2F on wire
    let raw = b"GET http://host/@anthropic%2Fvertex-sdk HTTP/1.1\r\nHost: host\r\n\r\n";
    let (canon, _) = canonicalize_request_target(
        "/@anthropic%2Fvertex-sdk",
        &CanonicalizeOptions { allow_encoded_slash: true, ..Default::default() },
    ).expect("canonicalization should succeed");
    assert_eq!(canon.path, "/@anthropic/vertex-sdk"); // 0x01 in memory representation

    let rewritten = rewrite_forward_request(raw, raw.len(), &canon.path, None).unwrap();
    let rewritten_str = String::from_utf8_lossy(&rewritten);
    // Must contain %2F on wire, NOT 0x01
    assert!(rewritten_str.contains("@anthropic%2Fvertex-sdk"),
        "wire bytes must re-encode 0x01 sentinel to %2F, got: {rewritten_str:?}");
}

Environment

  • OpenShell: v0.0.34
  • Cluster: forward proxy with allow_encoded_slash: true on HTTP endpoints
  • Sandbox policy: OpenClaw onboard with @anthropic-ai/vertex-sdk package download

Agent-First Checklist

  • I pointed my agent at the repo and had it investigate this issue
  • I loaded relevant skills (debug-openshell-cluster, openshell-cli)
  • My agent could not resolve this — the diagnostic above explains why

Metadata

Metadata

Assignees

No one assigned

    Labels

    state:triage-neededOpened without agent diagnostics and needs triage

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions