Security Engineering · Network Security

Application Proxy Firewalls and Parser Differentials

What a layer 7 proxy gains by terminating and re-originating a connection, why the parser it runs becomes an attack surface, and how two systems disagreeing about the same bytes creates a bypass.

A packet filter decides on a five-tuple and a small amount of protocol state. It cannot see that a request on port 443 asks for a path the policy forbids, because the path is inside the encrypted stream. An application proxy sees it, because the proxy is an endpoint of two separate connections: it terminates the client’s connection, parses the request, and opens its own connection to the server. Between those two connections it can inspect, rewrite, log, or refuse. The whole design follows from that split, including its main weakness.

Terminating the connection is what buys visibility

Nothing above the transport layer can be enforced without being a transport-layer endpoint. To read an HTTP request the proxy must complete the TCP handshake, read a full request per the framing rules, and then act. For plaintext HTTP that is enough. For TLS it means presenting a certificate for the requested name, negotiating a session with the client, and separately negotiating a session with the server, which is an interception rather than a pass-through. The client validates the proxy’s certificate against its own trust store, so the deployment only works if the organisation’s certificate authority is trusted by the clients. Where that trust does not exist, the client aborts the handshake and the proxy can do nothing but relay bytes.

For clients that use an explicit proxy, the mechanism is visible: the client sends a request line naming an absolute URI, or issues CONNECT for a tunnel and then performs TLS inside it. For transparent interception the proxy must be in the path and must decide from the destination port and, increasingly, from the server name indication extension carrying the requested hostname whether to intercept. That branch point is protocol-dependent. A TLS 1.3 client always encrypts the certificate in the handshake, so the request for a name is a necessary step; a client that does not send the name leaves the proxy with only the destination address to reason about, and the standard allows that absence.

Because the proxy is a full endpoint, it also inherits endpoint state: it must hold a socket per client connection and per server connection, plus buffers on both sides. Concurrency limits therefore bind before bandwidth does in most deployments, and the memory cost per connection sets the ceiling on how many clients can be served simultaneously.

Two parsers, one message

The security-relevant consequence of termination is that the message is parsed twice by two different programs: once by the proxy, once by the origin server. If those two programs disagree about what the bytes mean, the boundary between them is not a policy boundary at all. This is the parser differential, and HTTP supplies the classic example.

The framing of an HTTP/1.1 message is determined by headers. RFC 9112 requires that when a message carries both Transfer-Encoding and Content-Length, the transfer encoding governs and the content length must be ignored. A proxy that implements this rule and a server that instead prefers Content-Length will each see a different message boundary in the same byte stream. If the proxy forwards the bytes unchanged after inspecting the message it understood, the server reads a second request that the proxy never inspected. Because the proxy’s connection to the server is usually reused for many client requests, that hidden request can be shaped to land in front of someone else’s request and carry their session context.

The same divergence appears wherever the specification offers latitude or where implementations extend it: obsolete line folding in header fields, whitespace before the colon, multiple Content-Length values, the handling of bare CR as a line terminator, and the question of whether a bare LF is accepted. RFC 9112 disallows several of these and tells intermediaries to reject rather than normalise, precisely because normalisation is where the two parsers diverge.

There is no general fix at the proxy layer. Rejecting malformed framing is the recommended behaviour and is what the specification asks for; rewriting the message into an unambiguous canonical form is the alternative, but the rewriting must be lossless with respect to what the server will parse, which is exactly what the proxy cannot guarantee for a server it does not control. Deployments that control both ends can enforce one canonical framing on both sides, which converts a differential into a rejection.

Request-line and target rewriting

Proxies change messages as well as inspect them. A transparent proxy typically converts an absolute target into an origin-form target, normalises the path, and may add forwarding headers so the origin can see the client address. Each of these is a place where the origin’s view and the enforcement decision diverge.

Path normalisation is the sharper case. The proxy decides whether to allow a request based on a path it has decoded and normalised; the server resolves the path against its filesystem or router table using its own decoding rules. Percent-encoding, path traversal segments, and duplicate separators are all interpreted by both, and any disagreement about the order of decoding and resolution produces either a false denial or a bypass. RFC 9112’s guidance is to keep the target as received where possible, and to avoid decoding steps that the next hop will undo differently.

Forwarding headers carry a related problem. A header that names the client address is only meaningful if the proxy is the only entity that can set it. If the proxy appends to a header the client already sent, or accepts one from the client, the origin’s notion of the client address is attacker-controlled. The trustworthy form is a header the proxy replaces and the origin trusts only from the proxy.

Encryption, pinning, and the parts the proxy cannot see

Terminating TLS at the proxy breaks any mechanism that authenticates the server to the client at the transport layer. Certificate pinning, where the client requires a specific public key or a key from a specific issuer, fails against a proxy that presents its own certificate, unless the pin configuration is changed to include the proxy’s authority, which removes the protection the pin provided. The same applies to mutual TLS, where the client certificate must be presented to the proxy rather than to the server; the proxy must then be the party that validates it, and the server learns the client’s identity only from what the proxy asserts.

Protocol features designed to reduce middlebox visibility land on the same boundary. Encrypted client hello removes the server name from the observable handshake, so a proxy that decides interception policy from that name has nothing to read and must fall back to destination address, which fails for shared hosting. Certificate transparency constrains issuance but does not prevent a proxy from presenting a certificate the client’s trust store accepts.

The residual traffic is a real category: certificate validation endpoints, pinned clients, and telemetry channels that refuse interception must be excluded, and the exclusion is a policy hole by definition. Its size is the honest measure of what the proxy deployment actually covers.

Where the proxy sits relative to the filter

The layers compose. A proxy is reached through a filtering device, so the rule set decides whether the client may open a connection to the proxy at all, and the proxy’s own policy decides what may pass over that connection. Two independent decisions, usually expressed in two different languages, and the effective policy is their intersection. Most bypasses reported against proxy deployments are failures of the outer decision, namely a client reaching the origin directly on a path the filter permits.

Below the proxy, isolation is a separate question again. A proxy does not restrict which internal hosts can reach each other; that is the role of segment boundaries described in VLAN Segmentation and the Limits of Layer 2 Isolation, or of per-workload policy as in Microsegmentation and Policy at the Workload Boundary. A proxy is a control on the path between a client and a server it has not been permitted to reach directly, and its value is exactly the visibility that terminating the connection provides.

References

  1. RFC 9110: HTTP Semantics — IETF
  2. RFC 9112: HTTP/1.1 — IETF
  3. RFC 8446: The Transport Layer Security (TLS) Protocol Version 1.3 — IETF