# 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 `<!--#exec "..."-->` 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 `<!--#exec "id; uname -a" -->`, 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:<md5>`) 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 `<!--#exec "id; uname -a" -->`.
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.
