Phage is a security research tool for fuzzing and racing HTTP/3 (over QUIC)
servers, and for evolving HTTP/3-to-HTTP/1 request-smuggling desync vectors. It
implements the Quic-Fin-Sync race-condition primitive and layers an
evolutionary search on top of it.
Phage began as CyberArk Labs'
QuicDrawH3 (the Quic-Fin-Sync racing
client by Maor Abutbul), originally published in "Racing and Fuzzing
HTTP/3".
This fork adds two things on top of that base:
phage.evo, a coverage-guided, quality-diverse evolutionary engine that searches the H1/H2/H3 framing space for desyncs. A test case is a genome of framing operations rather than a byte string, so every case is sendable by construction and the same genome runs over all three protocol versions. See docs/EVO.md.matrix/, a framing honor matrix. A desync is a disagreement between two parsers, so instead of testing proxy-backend pairs it measures each half independently and joins them into predicted pairs. n fronts and m backends give n*m predictions from n+m measurements. See the matrix section.
Phage reaches one layer below where other HTTP/3 tooling stops: it can emit raw
QUIC transport frames, including RESET_STREAM_AT from the reliable-stream-reset
draft, which aioquic does not implement.
Research written up in Half a vulnerability each.
- Phage
- Phage-UI
- Framing honor matrix
- Evolutionary desync search
- Research
- Contributing
- Limitations
- Known issues
- License
- Credits
- Contact
- Implements the
Quic-Fin-Syncon HTTP3 (over QUIC), for race-condition testing. - Supports fuzzing multiple requests with the
FUZZand wordlist (-wargument) mechanisms. - Custom HTTP headers functionality (
-Hargument).- Note: Custom headers are converted to lowercase since we have seen issues with some server implementations.
- Supports SSLKEYLOGFILE (
-largument) for TLS decryption/inspection via packet analyzers such as Wireshark. - Based on aioquic (http3_client)
- aioquic is a library for the QUIC network protocol in Python.
- It features a minimal TLS 1.3 implementation, a QUIC stack, and an HTTP/3 stack.
Added by this fork:
- Evolutionary desync search (
phage.evo): MAP-Elites over a genome of framing ops, 36 mutation operators, a differential oracle with a built-in negative control, and auto-minimization of every hit. - One genome, three protocols: the same framing genome drives HTTP/1, HTTP/2 (raw HPACK) and HTTP/3, so a primitive found at one layer is immediately testable at the others.
- QUIC transport-state genes: connection-ID rotation (
Migrate), TLS key update (KeyUpdate), andRESET_STREAM_AT(ResetStreamAt), the reliable-stream-reset frame that lets a sender shrink the delivered body length after the bytes are already on the wire. aioquic does not implement that frame;phage.evo.quic_extemits it raw. - 0-RTT early-data mode: resume a session and drive a genome as early data, reporting whether real 0-RTT keys were obtained rather than assuming it.
- Framing honor matrix (
matrix/): measures which servers honor and which proxies forward each malformed framing header, then predicts the pairs that desync.
Prerequisite:
- python >=3.9
- pip3
The easiest way to install Phage is to run:
pip install phageThe easiest way to install Phage-UI is to run:
pip install phage[ui]
phage-ui -hphage -hIf there are no wheels for your system or if you wish to build Phage from source.
Clone the repository:
git clone https://github.com/UncleJ4ck/Phage.git
python3 -m build
pip install .\dist\phage-<VERSION>.tar.gzInstall module dependencies. (You may prefer to do this within a Virtual Environment)
phage -hphage <https://http3_server.com/path>HTTP POST requests are determined by using the -d argument followed by
the HTTP POST data to be sent.
phage <https://http3_server.com/path> -d '{"key":"value"}'log secrets to a file, for use with Wireshark
To inspect the traffic in wireshark: Open Wireshark → Edit → Preferences → Protocols → TLS and set "(Pre)-Master-Secret log filename" to the full path of secrets.log
Using the verbose (-v) output will log (print) the request data to be
sent and the HTTP response content.
In the case of GET requests (no -d argument supplied), the request URL
(:path) will be logged (printed).
To use the same request multiple times (using the Quic-Fin-Sync /
single-packet), use the -tr/--total-requests argument.
Note: If a WORDLIST (-w) argument is specified, this argument
(-tr TOTAL_REQUESTS) is overridden by the wordlist number of lines.
phage <https://http3_server.com/path> -d '{"key":"value"}' -H 'Authorization: bearer eyJ...' -tr 12phage <https://http3_server.com/path> -d '{"key":"value"}' -H 'Authorization: bearer eyJ...' -H 'content-type: application/json' -l /m2a/ssl_key_log_file.log -tr 12Repeat the same request 12 times (-tr 12), use Quic-Fin-Sync, log (-l) TLS secrets, and print verbose (-v) output including HTTP response content
phage <https://http3_server.com/path> -d '{"key": "value"}' -H 'Authorization: bearer eyJ...' -H 'content-type: application/json' -l /m2a/ssl_key_log_file.log -tr 12 -vFuzzing in Phage is based on a simple concept, like other web fuzzers
(Ffuf,
Wfuzz), go over the data section
(-d), and replace any reference to the FUZZ keyword with the value
given in the wordlist (-w) as the payload.
To define fuzzing, use the wordlist (-w/--wordlist) argument with
the FUZZ keyword anywhere in the DATA (-d argument) section.
Note: If the payload (-d) does not include the FUZZ keyword, the
same data will be sent according to the number of lines in the
wordlist file.
Use Quic-Fin-Sync, go over the data section (-d), and replace any reference to the FUZZ keyword with the value given in the wordlist file (-w) as the payload
phage <https://http3_server.com/path> -w path/to/wordlist -d '{"example_key":"FUZZ"}'Phage-UI is an HTTP/3 request editor: a GUI for Phage's fuzzing and racing client.
The easiest way to install Phage-UI is to run:
pip install phage[ui]
phage-ui -hSend a basic request to an HTTP/3 server:
phage-ui https://example.comAdvanced Tab
The following options can be set by the advanced tab
Option Description
-l, --secrets-log TLS secrets file (for Wireshark)
-v, --verbose Verbose output
Results Tab:
To fuzz an HTTP/3 endpoint, you need:
- A wordlist file (
-w) - contains payloads to test (one per line) - The
FUZZkeyword in your data - gets replaced by each wordlist entry
phage-ui https://example.com -w path/to/wordlist -d '{"example_key":"FUZZ"}'The FUZZ keyword in {"example_key":"FUZZ"} will be replaced with
each line from your wordlist file.
Phage-UI parameters are imported to the UI.
Option Description
-d, --data HTTP POST data (use FUZZ for wordlist
substitution)
-H, --header Custom header (repeatable)
-b, --cookie Custom cookie header
-w, --wordlist Fuzzing wordlist file
-tr, --total-requests Number of concurrent requests (race testing)
-l, --secrets-log TLS secrets file (for Wireshark)
-v, --verbose Verbose output
Note: "copy-as-curl compatible" meaning common curl arguments (-d,-H,-b) are supported by Phage-UI.
A request smuggling bug is not a property of a proxy. It is a property of a pair:
a front that draws the message boundary one way and a back that draws it another.
Testing pairs costs one experiment per pair and tells you nothing about the pair
you did not try, so matrix/ measures the two halves separately.
- Back half: which malformed framing values does a server honor? Signal is the number of HTTP responses it emits for one carrier request that hides a second request behind a zero-length chunk. Two responses means it de-chunked and framed the hidden bytes as a request of their own.
- Front half: which values does a proxy forward next to a
Content-Lengthinstead of acting on them? Signal is the exact request head the proxy emits to a byte-recording origin. - Join: a pair is predicted to desync when the front forwards a value the back honors.
python matrix/run_matrix.py # back half -> matrix/MATRIX.md, matrix/results.json
python matrix/run_fronts.py # front half -> matrix/fronts.json
python matrix/pairs.py # join -> matrix/PAIRS.mdThe two measurement scripts take --only <substring> to run a subset;
pairs.py just joins the JSON the other two wrote. Every backend and front runs
as a container bound to loopback.
One entry in matrix/backends.py (image, port, a trivial app) or
matrix/fronts.py (image, port, a config template). One entry in VARIANTS adds
a framing value and multiplies it across the whole population.
Counting responses can only see a second framed request on a connection the
server keeps open, so a backend that closes after one response can never make the
counter reach two. Every row is therefore gated on a pipelining control that must
return two responses. A row that fails it is reported UNTRUSTED, never as safe:
a negative from an instrument that has not been shown to produce a positive is not
evidence of absence, it is an untested instrument.
The front harness carries the same discipline as a permanent fixture: a known-vulnerable proxy sits in the population so that if the harness silently breaks, the calibration row goes quiet first.
Predictions are hypotheses. PAIRS.md says so. A predicted pair still has to
be fired end to end and confirmed against a negative control before it is a
vulnerability.
python -m phage.evo --host 127.0.0.1 --port 4433 \
--echo-log lab/logs/echo.jsonl --generations 200 --raw--raw hand-builds the frames instead of going through a conformant client, so a
Content-Length that contradicts the body, or a header a polite client would
refuse to send, actually reaches the wire. A saved hit replays with
--replay poc.json. Labs are under lab_*/; they are local-only and bind to
loopback.
- CVE-2026-33555, HAProxy HTTP/3 to HTTP/1 standalone-FIN desync. Fixed in
HAProxy 3.0.19. The
Fingene is that primitive, and the lab underlab_h3cve/reproduces it with a negative control. - sozu / kawa
Transfer-Encodingsmuggling, a regression of sozu#726. kawa 0.6.8 selected chunked framing with a suffix-only compare and no OWS trim, soTransfer-Encoding: chunked\twas forwarded alongsideContent-Length. Reported 2026-07-10, fixed in kawa PR #19, shipped in kawa 0.7.0 and sozu 2.2.0. Advisory: rustsec/advisory-db#3142. - Honest negatives. QUIC transport-state events (connection-ID rotation, key update) cause no desync on HAProxy, Caddy, nginx or Envoy: they sit below the HTTP framing layer, so a proxy that binds request state to the stream is immune by construction. QPACK blocked decoding buffers rather than partially forwarding, so it is a memory/DoS primitive and not a smuggling one. Both were killed with live tests rather than argued away.
RESET_STREAM_ATretroactive truncation is a primitive, not a finding. Every shipping stack tested rejects the frame, and Google QUICHE implements it but ships it disabled. The mechanism is demonstrated against a reference downgrader inlab_h3cve/reference_downgrader.py, which is code in this repo, not anything you run in production.
Write-up: Half a vulnerability each.
We welcome contributions of all kinds to this repository. For instructions on how to get started and descriptions of our development workflows, please see our contributing guide
- The
Quic-Fin-Syncis mostly effective in POST requests (using the-dargument).- GET requests will benefit from the mechanism, but according to our tests, only a few requests "fit" on a single QUIC packet.
- The fuzzing mechanism (
FUZZand--wordlist/-w) only works in POST messages data or in the GET request URL (:path) argument. - Currently, the fuzzing mechanism only works once, meaning if the
data argument is supplied (
-d), we assume fuzzing on the POST data, supplying theFUZZkeyword in the URL (:path) will result in sending the URL (:path) as-is (including theFUZZkeyword). - We do not support multiple different domains in the current version. (For different paths, you can use the FUZZ keyword in the URL's path part)
- On DNS error - the following error returned: "socket.gaierror: [Errno 11001] getaddrinfo failed"
Copyright (c) 2025 CyberArk Software Ltd. All rights reserved This
repository is licensed under the Apache-2.0 License - see
LICENSE for more details.
Phage is a fork of CyberArk Labs'
QuicDrawH3, the Quic-Fin-Sync
racing client by Maor Abutbul, published in "Racing and Fuzzing
HTTP/3".
The phage.evo evolutionary engine and the HTTP/3-to-HTTP/1 downgrade
research are additions on top of that base.
Built on aioquic.
Open a GitHub issue for feature requests or bugs.



