← All writeups
0-Day DisclosureFeb 11, 2026· 8 min read

LOSEC-2026-014: Pre-Auth RCE in Aperture EdgeProxy via Header Normalization Desync

A parsing disagreement between Aperture EdgeProxy's HTTP/1.1 front end and its HTTP/2 admin backend lets an unauthenticated attacker smuggle a template into the control plane and land remote code execution as root.

GH

@ghostframe

LoSec Research

Summary

Aperture EdgeProxy is a popular L7 reverse proxy that terminates client TLS over HTTP/1.1 and multiplexes upstream traffic to an internal control plane over HTTP/2. We found that the two parsers disagree on how a folded, over-long X-Forwarded-Host header is normalized. That disagreement is enough to inject a fully attacker-controlled header into the admin backend — which happens to render it through a Jinja-style template. The result is unauthenticated remote code execution as root on the proxy node.

This is an illustrative advisory using a fictional vendor and product. The techniques are real; Aperture EdgeProxy is not.

The desync

The front end accepts obs-fold (line-folded) headers for backward compatibility and rewrites them into a single logical header before forwarding. The rewrite loop truncates at an internal 8 KiB scratch buffer but never re-validates the terminator. When a header value crosses the buffer boundary at exactly the wrong offset, the trailing \r\n of the folded line survives into the HTTP/2 HEADERS frame as literal bytes.

Because the HTTP/2 encoder treats the value as opaque, those embedded CRLF bytes are re-interpreted by the backend as a new header field:

GET / HTTP/1.1
Host: edge.internal
X-Forwarded-Host: aaaa...(8150 bytes)...aaaa
  \r\nX-Admin-Template: {{ config }}

The front end sees one folded header. The backend sees two — and the second lands in the admin router's trusted namespace.

From injected header to RCE

The control plane exposes a diagnostics endpoint that echoes X-Admin-Template through its templating engine to render node status banners. It was never meant to be reachable pre-auth; the whole design assumed only the front end could set X-Admin-*. Server-side template injection follows directly:

X-Admin-Template: {{ ''.__class__.__mro__[1].__subclasses__() }}

Enumerating subclasses gives us a Popen gadget. The final payload:

X-Admin-Template: {{ cycler.__init__.__globals__.os.popen('id').read() }}

Response banner:

node-status: uid=0(root) gid=0(root) groups=0(root)

No credentials, no second request, no ASLR to defeat. The template engine runs in the same privileged process that binds :443.

Why the parsers disagreed

The root cause is a classic split-brain. Two properties combined to make it exploitable:

  • Length-truncation without re-parse. The scratch buffer bounded the copy but the CRLF scan ran on the original stream offset, so a boundary-straddling fold was silently preserved.
  • Implicit trust of a header prefix. X-Admin-* was authenticated by topology ("only the front end can reach the backend") rather than by a signature or token.

Either one alone is survivable. Together they are a pre-auth RCE.

Detection

You can hunt this in access logs without the patch. Look for folded headers whose reconstructed length lands within a few bytes of 8192, especially on X-Forwarded-*:

awk '/X-Forwarded-Host/ { if (length($0) > 8100 && length($0) < 8300) print }' access.log

At the network layer, a Suricata rule flagging embedded CRLF in HTTP/2 header block fragments forwarded from an HTTP/1.1 origin catches the smuggling primitive regardless of the final gadget.

Remediation

Upgrade to EdgeProxy 4.7.2, which:

  1. Re-scans the normalized value for control bytes after buffering, rejecting any header containing \r or \n post-fold.
  2. Signs internal X-Admin-* headers with a per-boot HMAC so topology is no longer the trust boundary.
  3. Sandboxes the diagnostics renderer with no os/subprocess globals.

If you cannot patch immediately, disable obs-fold acceptance (http.legacy_folding = off) and add a WAF rule dropping any request whose header block exceeds 8 KiB.

Disclosure timeline

  • 2026-01-06 — Reported to vendor with PoC.
  • 2026-01-08 — Triaged, severity confirmed (CVSS 9.8).
  • 2026-01-29 — Patch shipped in 4.7.2.
  • 2026-02-11 — Public advisory (this post).

Takeaway

Proxy chains multiply parsers, and every parser boundary is a place where two programs can disagree about where a header ends. When one side of that boundary is your control plane, a normalization bug stops being cosmetic. We keep finding these by fuzzing the seam between components rather than either component alone — the interesting bugs live in the disagreement.

#rce#http-desync#request-smuggling#ssti#disclosure
Share