Skip to content

bug: forward proxy forwards unevaluated pipelined request bytes #2251

Description

@pimlock

Agent Diagnostic

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

  1. 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.
  2. 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

  1. Start a raw TCP server on the host that records every received byte before responding.
  2. Create a sandbox policy that permits only GET /allowed on that server.
  3. 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.1
Host: host.openshell.internal:19090

GET /next HTTP/1.1
Host: host.openshell.internal:19090
  1. Observe that the upstream TCP server receives:
GET /allowed HTTP/1.1
Host: host.openshell.internal:19090
Connection: close
Via: 1.1 openshell-sandbox

GET /next HTTP/1.1
Host: host.openshell.internal:19090
  1. 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

Logs

captured 168 bytes
b'GET /allowed HTTP/1.1\r\nHost: host.openshell.internal:19090\r\nConnection: close\r\nVia: 1.1 openshell-sandbox\r\n\r\nGET /next HTTP/1.1\r\nHost: host.openshell.internal:19090\r\n\r\n'

HTTP:GET ALLOWED -> GET http://host.openshell.internal:19090/allowed [policy:capture_server engine:l7]
HTTP:GET ALLOWED -> GET http://host.openshell.internal:19090/allowed [policy:capture_server engine:opa]

No policy event was emitted for /next.

Definition of Done

  • A high-level regression test sends A and B in one TCP write through the plain HTTP forward proxy.
  • The upstream never receives bytes beyond A's framed message boundary.
  • Request B is not interpreted as A's body by middleware.
  • Valid fixed-length and chunked request bodies remain intact.
  • The selected reject-or-drop behavior is documented as the forward proxy's explicit non-pipelining contract.

Agent-First Checklist

  • I pointed my agent at the repo and had it investigate this issue.
  • I loaded the relevant OpenShell skills.
  • I checked the latest OpenShell release and verified the affected source pattern is present.
  • I searched existing issues for possible duplicates.
  • The issue remains unresolved because the proxy needs an explicit reject-or-drop implementation and regression coverage.

Metadata

Metadata

Assignees

Labels

area:supervisorProxy and routing-path worktopic:l7Application-layer policy and inspection work

Type

No type

Fields

No fields configured for issues without a type.

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions