# REPRO-2026-00229: CivetWeb PUT + SSI #exec RCE ## Summary Status: published Severity: high Type: security Confidence: high ## Identifiers REPRO ID: REPRO-2026-00229 CVE: CVE-VINEXT-CIVETWEB-PUT-SSI-RCE ## Package Name: civetweb/civetweb Ecosystem: github Affected: Unknown Fixed: Unknown ## Root Cause # RCA Report: CivetWeb Authenticated PUT + SSI `#exec` RCE ## Summary CivetWeb’s default `ssi_pattern` causes files ending in `.shtml` or `.shtm` to be processed as Server Side Includes (SSI). When the `put_delete_auth_file` option is enabled, CivetWeb accepts authenticated HTTP PUT uploads. Because files uploaded via PUT live in the configured `document_root`, a subsequent GET request for a `.shtml` upload is handled by the SSI engine. The SSI parser calls `do_ssi_exec()` on `` tags, which passes the command string directly to `popen()`. An authenticated attacker can therefore upload a `.shtml` file containing an SSI `#exec` directive and execute arbitrary shell commands on the server by requesting that file. ## Impact - **Package/component affected:** CivetWeb HTTP server (`civetweb` executable, `src/civetweb.c`). - **Affected versions:** The ticket names commit `588860e3`. That specific commit has a build-breaking syntax error in `get_request()` and does not compile. The immediately preceding parent commit (`588860e3^1`, also known as `3309a6c`) is the last working master revision and still contains the vulnerable `do_ssi_exec` / `handle_put_file` code path. The same behavior is present in `588860e3` once the unrelated syntax error is corrected. - **Risk level and consequences:** High. Any user with a valid digest credential for the `put_delete_auth_file` realm can upload and execute arbitrary shell commands as the CivetWeb process user, leading to full host compromise. ## Impact Parity - **Disclosed/claimed maximum impact:** Authenticated remote code execution via PUT upload of `.shtml` followed by GET. - **Reproduced impact from this run:** The reproduction script authenticated with digest credentials, PUT an `.shtml` payload containing ``, and a subsequent GET returned the output of `id` and `uname -a` in the HTTP response body. This proves the server executed attacker-controlled shell commands through the real HTTP API. - **Parity:** `full` - **Not demonstrated:** N/A — the claimed code-execution path was demonstrated end-to-end through the real HTTP API. ## Root Cause The root cause is the interaction of two default/documented features without sufficient isolation: 1. **Authenticated PUT/DELETE support.** When `put_delete_auth_file` is configured, CivetWeb allows any user with a valid digest credential to write arbitrary files into the `document_root` (`handle_put_file()` in `src/civetweb.c`). 2. **SSI `#exec` processing.** The default `ssi_pattern` is `**.shtml$|**.shtm$`. When a requested file matches this pattern, `send_ssi_file()` scans the file for `` tags. If the tag starts with `exec` and `NO_POPEN` is not defined, it calls `do_ssi_exec(conn, buf + 9)`: ```c #if !defined(NO_POPEN) static void do_ssi_exec(struct mg_connection *conn, char *tag) { char cmd[1024] = ""; struct mg_file file = STRUCT_FILE_INITIALIZER; if (sscanf(tag, " \"%1023[^\"]\"", cmd) != 1) { mg_cry_internal(conn, "Bad SSI #exec: [%s]", tag); } else { cmd[1023] = 0; if ((file.access.fp = popen(cmd, "r")) == NULL) { mg_cry_internal(conn, "Cannot SSI #exec: [%s]: %s", cmd, strerror(ERRNO)); } else { send_file_data(conn, &file, 0, INT64_MAX, 0); /* send static file */ pclose(file.access.fp); } } } #endif /* !NO_POPEN */ ``` The command string is extracted from the SSI tag and passed unmodified to `popen()`, which runs it with the privileges of the CivetWeb process. There is no additional authorization check for `#exec` beyond the SSI file pattern match, and no restriction on which files created via PUT may be treated as SSI. - **Link to fix commit:** No upstream fix commit was identified in the repository at the time of this run. The current master (`588860e3`) still contains the `do_ssi_exec` code path and is only prevented from compiling by an unrelated syntax error in `get_request()`. ## Reproduction Steps 1. **Build the vulnerable server:** Run `bundle/repro/reproduction_steps.sh`. The script reads `bundle/project_cache_context.json`, clones or reuses the CivetWeb repository from the project cache, resolves the ticket-named commit `588860e3`, and uses its working parent `588860e3^1` (`3309a6c`) because the named commit does not compile. It builds two binaries: one default (vulnerable) and one with `-DNO_POPEN` (control). 2. **Start the server:** The script creates a digest password file (`admin:mydomain.com:`) and starts CivetWeb with `listening_ports`, `document_root`, `put_delete_auth_file`, and `authentication_domain`. 3. **Upload the payload:** The script performs an authenticated HTTP PUT to `/pwn.shtml` with the body ``. 4. **Trigger execution:** The script sends an authenticated HTTP GET to `/pwn.shtml`. On the vulnerable build, the response body contains the output of the executed commands. On the `-DNO_POPEN` build, the response body is empty because the `#exec` directive is not implemented. 5. **Expected evidence:** - `bundle/artifacts/vulnerable-attempt1/get_body.txt` contains `uid=...` and `Linux ...`. - `bundle/artifacts/fixed-attempt1/get_body.txt` does not contain either string. - `bundle/logs/reproduction_steps.log` shows two vulnerable attempts returning command output and two fixed attempts returning no command output. ## Evidence - **Log file:** `bundle/logs/reproduction_steps.log` - **Vulnerable GET response body:** `bundle/artifacts/vulnerable-attempt1/get_body.txt`: ``` uid=1000(vscode) gid=1000(vscode) groups=1000(vscode),962(962) Linux d778bdddc001 7.0.14-arch1-1 #1 SMP PREEMPT_DYNAMIC Sat, 27 Jun 2026 16:15:10 +0000 x86_64 GNU/Linux ``` - **Vulnerable GET response headers:** `bundle/artifacts/vulnerable-attempt1/get_headers.txt` shows `HTTP/1.1 200 OK` and `Content-Type: text/html`. - **Fixed GET response body:** `bundle/artifacts/fixed-attempt1/get_body.txt` is empty, confirming the `#exec` path is disabled when `NO_POPEN` is defined. - **HTTP headers:** `bundle/artifacts/vulnerable-attempt1/put_headers.txt` and `get_headers.txt` show `HTTP/1.1 200 OK` for both PUT and GET. - **Server logs:** `bundle/artifacts/vulnerable-attempt1/server.log` and `bundle/artifacts/fixed-attempt1/server.log` show the CivetWeb startup and shutdown. - **Environment:** Reproduced on Linux x86_64 with gcc, using CivetWeb built from source at commit `3309a6c` (`588860e3^1`). ## Recommendations / Next Steps - **Short-term mitigation:** Build CivetWeb with `-DNO_POPEN` to disable SSI `#exec` entirely, or disable PUT/DELETE by not setting `put_delete_auth_file`. Alternatively, restrict `ssi_pattern` so that untrusted upload directories cannot match it. - **Proper fix:** Remove or gate the SSI `#exec` directive behind an explicit opt-in option (e.g., `ssi_exec_enabled yes`) that defaults to `no`. When enabled, restrict it to a dedicated, non-upload directory and/or require additional authorization. Alternatively, do not allow files uploaded via PUT to match `ssi_pattern` unless explicitly whitelisted. - **Upgrade guidance:** There is no known upstream fixed version at the time of this report. Users should apply the `-DNO_POPEN` build flag or disable PUT/DELETE until a patched release is available. - **Testing recommendations:** Add an integration test that uploads an `.shtml` file with `#exec` and verifies that no command is executed, and that the response body does not contain shell command output. ## Additional Notes - **Idempotency:** The reproduction script was run twice consecutively from the same project-cache state and produced identical confirmation results in both runs. - **Commit discrepancy:** The ticket names commit `588860e3` as the vulnerable version. That commit has a build-breaking syntax error in `src/civetweb.c` (`get_request()`: `if (h_chunk != NULL) && ...` instead of `if ((h_chunk != NULL) && ...)` plus an undeclared `cl` variable). The reproduction therefore uses the immediately preceding working commit `588860e3^1` (`3309a6c`), which is the parent of `588860e3` and contains the same vulnerable SSI code path. - **Limitations:** The reproduction demonstrates authenticated RCE against a locally running CivetWeb instance. Actual deployments may differ in user privilege, authentication domain, or file-system layout, but the underlying SSI `#exec` mechanism is the same. ## Reproduction Details Reproduced: 2026-07-04T22:08:20.436Z Duration: 2067 seconds Tool calls: 374 Turns: Unknown Handoffs: 6 ## Quick Verification Run one of these commands to verify locally: pruva-verify REPRO-2026-00229 pruva-verify CVE-VINEXT-CIVETWEB-PUT-SSI-RCE Or open in GitHub Codespaces (zero-friction, auto-runs): https://github.com/codespaces/new?ref=repro/REPRO-2026-00229&repo=N3mes1s/pruva-sandbox Or download and run the script manually: curl -O https://api.pruva.dev/v1/reproductions/REPRO-2026-00229/artifacts/bundle/repro/reproduction_steps.sh chmod +x reproduction_steps.sh ./reproduction_steps.sh WARNING: Run in a sandboxed environment. This exploits a real vulnerability. ## References - NVD: https://nvd.nist.gov/vuln/detail/CVE-VINEXT-CIVETWEB-PUT-SSI-RCE - Source: https://github.com/civetweb/civetweb ## Artifacts - bundle/repro/reproduction_steps.sh (reproduction_script, 10167 bytes) - bundle/repro/rca_report.md (analysis, 8535 bytes) - bundle/vuln_variant/reproduction_steps.sh (reproduction_script, 11321 bytes) - bundle/vuln_variant/rca_report.md (analysis, 9716 bytes) - bundle/coding/proposed_fix.diff (patch, 1181 bytes) - bundle/vuln_variant/source_identity.json (other, 788 bytes) - bundle/vuln_variant/root_cause_equivalence.json (other, 1130 bytes) - bundle/coding/artifacts/patched_get_headers.txt (other, 268 bytes) - bundle/repro/validation_verdict.json (other, 784 bytes) - bundle/repro/runtime_manifest.json (other, 1164 bytes) - bundle/logs/reproduction_steps.log (log, 4981 bytes) - bundle/logs/vuln_variant.log (log, 8996 bytes) - bundle/vuln_variant/artifacts/vulnerable-chunked/get_body.txt (other, 169 bytes) - bundle/vuln_variant/artifacts/vulnerable-webdav/get_body.txt (other, 169 bytes) - bundle/vuln_variant/artifacts/vulnerable-shtm/get_body.txt (other, 169 bytes) - bundle/vuln_variant/variant_manifest.json (other, 2536 bytes) - bundle/vuln_variant/validation_verdict.json (other, 907 bytes) - bundle/vuln_variant/patch_analysis.md (documentation, 5831 bytes) - bundle/vuln_variant/runtime_manifest.json (other, 1196 bytes) - bundle/vuln_variant/artifacts/vulnerable-chunked/get_headers.txt (other, 268 bytes) - bundle/vuln_variant/artifacts/vulnerable-webdav/get_headers.txt (other, 268 bytes) - bundle/vuln_variant/artifacts/vulnerable-shtm/get_headers.txt (other, 268 bytes) - bundle/vuln_variant/artifacts/fixed-chunked/get_body.txt (other, 0 bytes) - bundle/vuln_variant/artifacts/fixed-chunked/get_headers.txt (other, 268 bytes) - bundle/vuln_variant/artifacts/fixed-webdav/get_body.txt (other, 0 bytes) - bundle/vuln_variant/artifacts/fixed-webdav/get_headers.txt (other, 268 bytes) - bundle/vuln_variant/artifacts/fixed-shtm/get_body.txt (other, 0 bytes) - bundle/vuln_variant/artifacts/fixed-shtm/get_headers.txt (other, 268 bytes) - bundle/coding/verify_fix.sh (other, 4353 bytes) - bundle/coding/summary_report.md (documentation, 4466 bytes) - bundle/logs/verify_fix.log (log, 5783 bytes) - bundle/coding/artifacts/patched_get_body.txt (other, 0 bytes) ## API Access - JSON: https://api.pruva.dev/v1/reproductions/REPRO-2026-00229 - Script: https://api.pruva.dev/v1/reproductions/REPRO-2026-00229/artifacts/bundle/repro/reproduction_steps.sh - Web: https://pruva.dev/r/REPRO-2026-00229 ## For AI Assistants To help users with this reproduction: 1. The `pruva-verify` command is the simplest way to run the reproduction 2. The script downloads and runs automatically in an isolated temp directory 3. Always recommend running in a VM, container, or disposable environment 4. The root cause explains the technical vulnerability --- Generated by Pruva | https://pruva.dev