
Controlled NGINX HTTP/2 frame injection lab for CVE-2026-42926 patch validation and defensive research
A controlled cybersecurity lab for validating and comparing behavior related to CVE-2026-42926, an HTTP/2 frame injection issue affecting specific NGINX versions when a vulnerable proxy configuration is used.
This repository is intended for defensive research, patch validation, configuration auditing, and controlled lab reproduction only.
Classification: HTTP/2 frame injection Affected versions: NGINX
1.29.4through1.30.0Fixed versions: NGINX1.30.1+/1.31.0+
Use this project only in an isolated lab environment that you own or are explicitly authorized to test.
Do not run this against third-party systems, public infrastructure, shared environments, or production services without written authorization.
Recommended isolation:
The lab checks whether a target NGINX binary and configuration match the conditions needed to reproduce the issue, sends a crafted request to the test location, and inspects a controlled upstream logger for evidence that injected HTTP/2 frame-like bytes reached the upstream side.
The normal validation pattern is:
Expected result:
bashpython3phpFor the containerized workflow:
The sample config listens on port 80, which commonly requires root privileges.
For an unprivileged local lab, change listen 80; in nginx_vulnerable.conf to
an available high port such as 8080, then use the matching target URL in the
PHP command.
Make the shell scripts executable:
chmod +x nginx_config_verify.sh run_lab_comparison.sh
Confirm PHP has cURL support:
php -m | grep -i curl
Confirm each NGINX binary can print its version:
/path/to/nginx -V
The provided nginx_vulnerable.conf contains the required test pattern:
location /exploit {
proxy_pass http://127.0.0.1:8081;
proxy_http_version 2;
proxy_set_body $request_body;
proxy_set_header Host $host;
proxy_set_header Content-Length $content_length;
}
Important details:
proxy_http_version 2 enables HTTP/2 proxying to the upstream logger.proxy_set_body $request_body uses a client-controlled request body.client_max_body_size 20m allows the crafted 16 MiB request body used by the
validation script.127.0.0.1:8081 by default.The Docker-specific config in docker/nginx_vulnerable.docker.conf keeps the
same vulnerable proxy pattern but listens on container port 8080 and proxies
to the Compose service name upstream:8081.
The Dockerfile builds NGINX 1.29.4 from source and installs the PHP/Python
tooling required by the lab. Compose then runs three services from the same
image:
upstream: raw HTTP/2 frame loggernginx: vulnerable NGINX 1.29.4 using docker/nginx_vulnerable.docker.confrunner: one-shot PHP validation commandBuild the lab image:
docker compose build
Start the upstream logger and vulnerable NGINX:
docker compose up -d upstream nginx
Confirm the bundled NGINX version:
docker compose exec nginx nginx -V
Run the validation script inside the Compose network:
docker compose --profile run run --rm runner
The runner uses these in-container arguments:
php /lab/cve_2026_42926_lab.php \
http://nginx:8080/exploit \
/lab/upstream_logs \
/lab/nginx_config_verify.sh \
/usr/local/nginx/sbin/nginx \
/lab/docker/nginx_vulnerable.docker.conf \
/exploit
Generated upstream logs are written to the host directory:
./upstream_logs/
The NGINX service is also exposed to the host at:
http://localhost:8080/version
Stop and remove the lab containers:
docker compose down
Start the controlled upstream logger:
python3 upstream_frame_logger.py 8081 ./upstream_logs
In another terminal, start NGINX with the sample configuration:
/path/to/nginx -c "$PWD/nginx_vulnerable.conf"
Run the validation script:
php cve_2026_42926_lab.php \
http://localhost/exploit \
./upstream_logs \
./nginx_config_verify.sh \
/path/to/nginx \
"$PWD/nginx_vulnerable.conf" \
/exploit
Stop NGINX after the run:
/path/to/nginx -s stop
If you changed NGINX to listen on another port, update the first argument. For example:
php cve_2026_42926_lab.php http://localhost:8080/exploit ./upstream_logs ./nginx_config_verify.sh /path/to/nginx "$PWD/nginx_vulnerable.conf" /exploit
Set paths to the two NGINX binaries and run the comparison harness:
VULNERABLE_NGINX=/usr/local/nginx_1.29.4/sbin/nginx \
PATCHED_NGINX=/usr/local/nginx_1.30.1/sbin/nginx \
bash run_lab_comparison.sh
Optional configuration overrides:
VULNERABLE_NGINX=/path/to/vulnerable/nginx \
PATCHED_NGINX=/path/to/patched/nginx \
VULNERABLE_CONFIG="$PWD/nginx_vulnerable.conf" \
PATCHED_CONFIG="$PWD/nginx_vulnerable.conf" \
bash run_lab_comparison.sh
The comparison script writes:
vulnerable_result.txtpatched_result.txtupstream_logs_vulnerable/upstream_logs_patched/Use nginx_config_verify.sh directly when you only want to inspect whether a
configuration contains the vulnerable pattern:
./nginx_config_verify.sh /path/to/nginx "$PWD/nginx_vulnerable.conf" /exploit
The helper checks for:
proxy_http_version 2proxy_set_body with a variableclient_max_body_size of at least 16 MiBcve_2026_42926_lab.php accepts positional arguments:
php cve_2026_42926_lab.php <target_url> <upstream_log_dir> <config_script> <nginx_binary> <nginx_config> <location> [version_url]
Defaults:
cve_2026_42926_lab.php returns one of three verdicts:
Positive evidence requires the script to correlate the run marker, observed upstream frame header, injected-frame flag, and affected NGINX version.
The upstream logger writes JSON files named like:
frames_<timestamp>.json
Each log contains parsed HTTP/2 frame metadata, payload excerpts, byte offsets, injection detection flags, and run-correlation fields.
The comparison harness stores separate log directories for vulnerable and patched runs so evidence from the two executions does not mix.
If the PHP script reports that the config helper is not executable, run:
chmod +x nginx_config_verify.sh
If the request returns 413 Request Entity Too Large, increase
client_max_body_size to at least 16m; the sample config uses 20m.
If no upstream logs are created, check that:
upstream_frame_logger.py is running.127.0.0.1:8081./exploit location.If NGINX fails to bind to port 80, either run it with appropriate privileges
in a lab environment or change the config to a high port such as 8080.
If the comparison result is inconclusive, inspect vulnerable_result.txt,
patched_result.txt, and the corresponding upstream log directories for the
failed precondition.
This project is licensed under the MIT License. See LICENSE for details.
| File | Description |
|---|
README.md | Project documentation. |
LICENSE | MIT license. |
Dockerfile | Builds a self-contained lab image with NGINX 1.29.4, PHP CLI/cURL, Python, and the project scripts. |
docker-compose.yml | Starts the vulnerable NGINX service, upstream frame logger, and an optional validation runner. |
cve_2026_42926_lab.php | Main lab validation script. It checks version/config preconditions, sends the crafted request, inspects upstream logs, and returns a verdict. |
nginx_vulnerable.conf | Sample NGINX configuration containing the vulnerable proxying pattern used for both vulnerable and patched comparison runs. |
docker/nginx_vulnerable.docker.conf | Docker-specific NGINX config using the same vulnerable pattern and Compose service discovery. |
nginx_config_verify.sh | Helper that verifies whether a target NGINX config contains the required vulnerable proxy pattern. |
upstream_frame_logger.py | Controlled raw HTTP/2 upstream logger used to capture and inspect frames received from NGINX. |
run_lab_comparison.sh | Orchestrates vulnerable-vs-patched comparison runs. |
.dockerignore | Keeps generated logs and IDE metadata out of Docker build context. |
| Argument | Default |
|---|
target_url | http://localhost/exploit |
upstream_log_dir | ./upstream_logs |
config_script | ./nginx_config_verify.sh |
nginx_binary | nginx |
nginx_config | /etc/nginx/nginx.conf |
location | /exploit |
version_url | http://localhost/version |
| Verdict | Meaning | Exit code |
|---|
positive | Frame injection evidence was observed and correlated to the run. | 0 |
negative | Preconditions were met and no injection evidence was observed. | 1 |
inconclusive | One or more preconditions or evidence checks failed. | 2 |