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:
- Local VM
- Disposable container
- Private test host
- Non-routable lab network
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:
- Run the same vulnerable proxy configuration with a vulnerable NGINX build.
- Run the same vulnerable proxy configuration with a patched NGINX build.
- Compare the upstream evidence and script verdicts.
Expected result:
- Vulnerable build: injection evidence may be observed.
- Patched build: no injection evidence should be observed.
| 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. |
- Linux or macOS shell environment
bashpython3php- PHP cURL extension
- NGINX test binaries for the versions you want to compare
- Permission to bind the configured NGINX listen port
For the containerized workflow:
- Docker
- Docker Compose v2
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.shConfirm PHP has cURL support:
php -m | grep -i curlConfirm each NGINX binary can print its version:
/path/to/nginx -VThe 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 2enables HTTP/2 proxying to the upstream logger.proxy_set_body $request_bodyuses a client-controlled request body.client_max_body_size 20mallows the crafted 16 MiB request body used by the validation script.- The upstream logger listens on
127.0.0.1:8081by 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 NGINX1.29.4usingdocker/nginx_vulnerable.docker.confrunner: one-shot PHP validation command
Build the lab image:
docker compose buildStart the upstream logger and vulnerable NGINX:
docker compose up -d upstream nginxConfirm the bundled NGINX version:
docker compose exec nginx nginx -VRun the validation script inside the Compose network:
docker compose --profile run run --rm runnerThe 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 downStart the controlled upstream logger:
python3 upstream_frame_logger.py 8081 ./upstream_logsIn 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" \
/exploitStop NGINX after the run:
/path/to/nginx -s stopIf 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" /exploitSet 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.shOptional 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.shThe 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" /exploitThe helper checks for:
proxy_http_version 2proxy_set_bodywith a variableclient_max_body_sizeof at least 16 MiB
cve_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:
| 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 |
cve_2026_42926_lab.php returns one of three verdicts:
| 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 |
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.shIf 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.pyis running.- NGINX is proxying to
127.0.0.1:8081. - The target URL points to the configured NGINX listener.
- The request reached the
/exploitlocation.
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.