You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Skills loaded: openshell-cli, debug-openshell-cluster, generate-sandbox-policy, and create-github-issue.
OpenShell version tested: 0.0.82-dev.50+gc66f9ea4 on branch 1733-supervisor-middleware/pmlocek. The affected forward-proxy path was also verified directly on main.
Latest release checked: v0.0.82, published July 13, 2026. The release source still injects Connection: close, appends all bytes after the first header block, and documents that one L7 evaluation suffices.
Findings: a live Docker-backed sandbox sent an allowed request A followed by request B in one TCP write. The upstream raw TCP server received both requests on the same connection, while OpenShell logged policy evaluation only for A.
Remaining reason for filing: the forward proxy forwards bytes that are outside A's HTTP message boundary without parsing or independently enforcing them. Middleware did not introduce this architectural issue. It made it visible by interpreting the same leftover bytes as A's body.
Description
The plain HTTP forward proxy supports one request per connection and injects Connection: close, but it does not enforce the corresponding request boundary before forwarding bytes upstream.
The initial client read can contain a complete request A followed by some or all of a pipelined request B. OpenShell finds A's first \r\n\r\n, parses and evaluates only A, rewrites A from absolute-form to origin-form, and then appends every byte after A's header block to the upstream request. For a request with no Content-Length or Transfer-Encoding, those bytes are not a body. They belong to the next request.
Actual behavior: OpenShell forwards request B's bytes upstream after the allowed request A, despite never parsing B or applying B's L7 policy or middleware chain.
Expected behavior: Because the forward proxy does not support pipelining, it must never forward bytes beyond the end of the independently evaluated HTTP message. It should either reject the input or forward only A and discard the remainder before closing the connection.
Connection: close mitigates the impact because a compliant upstream must not process B. However, downstream compliance should not be the enforcement boundary. If an upstream or intermediary processes the additional request, B can bypass request-level L7 and middleware enforcement. The destination TCP connection is still the one selected for A, so this does not directly bypass A's destination host and port decision.
Relationship to middleware
This was discovered while reviewing middleware request-body buffering, but it is a separate issue already present on main.
Before middleware, the forward proxy already forwarded leftover bytes unchanged. Middleware merely made the bug more obvious by accidentally interpreting the leftover bytes as a request body and rebuilding the request with Content-Length. The middleware change did not introduce the architectural problem. It exposed it.
Fix options
Reject the request when bytes beyond its framed message boundary are already buffered. This is fail-closed, but it is a behavioral change. A client that pipelines today might receive a successful response for A because a compliant upstream discards B; after this change, A could fail.
Drop bytes beyond A's framed message boundary and complete A normally. Since pipelining is not supported and Connection: close already makes A the final request, this should preserve supported workloads while preventing unevaluated bytes from reaching upstream.
The boundary must be determined from HTTP framing, not from a TCP read: headers only for a no-body request, exactly Content-Length bytes for a fixed body, and through the terminal chunk and trailers for chunked encoding.
Reproduction Steps
Start a raw TCP server on the host that records every received byte before responding.
Create a sandbox policy that permits only GET /allowed on that server.
From a sandbox process, connect to the proxy from HTTP_PROXY and send the following bytes in one socket write:
GET http://host.openshell.internal:19090/allowed HTTP/1.1Host: host.openshell.internal:19090GET /next HTTP/1.1Host: host.openshell.internal:19090
Observe that the upstream TCP server receives:
GET /allowed HTTP/1.1Host: host.openshell.internal:19090Connection: closeVia: 1.1 openshell-sandboxGET /next HTTP/1.1Host: host.openshell.internal:19090
Observe that the sandbox logs contain L7 and OPA allow events only for GET /allowed. There is no evaluation event for GET /next.
Environment
OS: macOS 26.5.1
Docker: Docker Desktop engine 29.6.1
OpenShell: 0.0.82-dev.50+gc66f9ea4
Gateway: isolated plaintext Docker-backed development gateway built from the current branch
Latest release checked: v0.0.82; the affected source pattern is present
Possible duplicates checked: yes, open and closed GitHub issues were searched using multiple request-boundary and pipelining terms
Agent Diagnostic
openshell-cli,debug-openshell-cluster,generate-sandbox-policy, andcreate-github-issue.0.0.82-dev.50+gc66f9ea4on branch1733-supervisor-middleware/pmlocek. The affected forward-proxy path was also verified directly onmain.v0.0.82, published July 13, 2026. The release source still injectsConnection: close, appends all bytes after the first header block, and documents that one L7 evaluation suffices.pipelining,leftover bytes,forward proxy request boundary, andConnection: close. No duplicate was found. Issues feat: support plain HTTP forward proxy for private IP endpoints #155, sec(sandbox): L7 proxy accepts multiple Content-Length headers — enables HTTP request smuggling (CWE-444) #637, and sec(proxy): forward proxy path skips L7 inspection — policy enforcement gap on private IP endpoints #643 are related history but describe different problems.Description
The plain HTTP forward proxy supports one request per connection and injects
Connection: close, but it does not enforce the corresponding request boundary before forwarding bytes upstream.The initial client read can contain a complete request A followed by some or all of a pipelined request B. OpenShell finds A's first
\r\n\r\n, parses and evaluates only A, rewrites A from absolute-form to origin-form, and then appends every byte after A's header block to the upstream request. For a request with noContent-LengthorTransfer-Encoding, those bytes are not a body. They belong to the next request.Actual behavior: OpenShell forwards request B's bytes upstream after the allowed request A, despite never parsing B or applying B's L7 policy or middleware chain.
Expected behavior: Because the forward proxy does not support pipelining, it must never forward bytes beyond the end of the independently evaluated HTTP message. It should either reject the input or forward only A and discard the remainder before closing the connection.
Connection: closemitigates the impact because a compliant upstream must not process B. However, downstream compliance should not be the enforcement boundary. If an upstream or intermediary processes the additional request, B can bypass request-level L7 and middleware enforcement. The destination TCP connection is still the one selected for A, so this does not directly bypass A's destination host and port decision.Relationship to middleware
This was discovered while reviewing middleware request-body buffering, but it is a separate issue already present on
main.Before middleware, the forward proxy already forwarded leftover bytes unchanged. Middleware merely made the bug more obvious by accidentally interpreting the leftover bytes as a request body and rebuilding the request with
Content-Length. The middleware change did not introduce the architectural problem. It exposed it.Fix options
Connection: closealready makes A the final request, this should preserve supported workloads while preventing unevaluated bytes from reaching upstream.The boundary must be determined from HTTP framing, not from a TCP read: headers only for a no-body request, exactly
Content-Lengthbytes for a fixed body, and through the terminal chunk and trailers for chunked encoding.Reproduction Steps
GET /allowedon that server.HTTP_PROXYand send the following bytes in one socket write:GET /allowed. There is no evaluation event forGET /next.Environment
0.0.82-dev.50+gc66f9ea4v0.0.82; the affected source pattern is presentLogs
No policy event was emitted for
/next.Definition of Done
Agent-First Checklist