# REPRO-2026-00332: Fledge backup upload shell command injection ## Summary Status: published Severity: high CVSS: Unknown CWE: Unknown Type: security Confidence: high ## Identifiers REPRO ID: REPRO-2026-00332 CVE: CVE-2026-71284 ## Package Name: fledge-iot/fledge Ecosystem: github Affected: CVE.org lists versions 0 through 3.1.0 as affected; the ticket requires proof on 3.1.0. Fixed: Unknown ## Root Cause ## Summary Fledge 3.1.0 is vulnerable to authenticated OS command injection in the production backup-upload REST handler. An administrator can upload a syntactically valid `.tar.gz` archive whose first member starts with `fledge_backup_`, ends with `.db`, and contains shell metacharacters between those accepted strings. `upload_backup()` extracts that member, concatenates its path into `cp {source} {backup_path}`, and executes the result with `os.system()`. A real Fledge service consequently runs the embedded shell command as its `fledge` service user. ## Impact - **Product/component:** `fledge-iot/fledge`, specifically `python/fledge/services/core/api/backup_restore.py::upload_backup()` and `POST /fledge/backup/upload`. - **Affected release tested:** Fledge **3.1.0**, tag and commit `f90ffc2047ee49a380ada98a59fcc2985bd6a943` (database schema 75). The ticket reports versions through 3.1.0 as affected. - **Required attacker position:** An authenticated Fledge administrator with access to the backup-upload endpoint. - **Risk:** High. The administrator can execute arbitrary local shell commands with the identity and filesystem access of the Fledge service account. In the reproduced deployment that identity was UID 10001, user/group `fledge`. - **Consequence demonstrated:** Deterministic command execution through the network API, including attacker-selected file creation and content. ## Impact Parity - **Disclosed/claimed maximum impact:** Authenticated remote OS command execution as the Fledge service user. - **Reproduced impact:** Two fresh vulnerable Fledge 3.1.0 service instances accepted authenticated crafted uploads. Each spawned `/bin/sh -c` with the attacker-controlled member filename and created a distinct attacker-selected marker as `fledge`. - **Parity:** `full`. - **Not demonstrated:** No privilege escalation beyond the Fledge service account, lateral movement, persistence, or outbound network behavior was attempted or required. These are not part of the claimed maximum impact. ## Root Cause The handler applies validation to two different filename layers: 1. The outer multipart filename must start with `fledge_backup_` and end in `.tar.gz`. 2. At least one archive member must start with `fledge_backup_` and end in `.db` or `.dump`. For archives compatible with older Fledge versions, the handler calls `tar_file.extractall(temp_path)`, assigns `backup_file_name = tar_file_names[0]`, and builds `source = temp_path + "/" + backup_file_name`. The prefix/suffix checks do not reject shell syntax inside the accepted member name. The vulnerable code then performs: ```python cmd = "cp {} {}".format(source, backup_path) ret_code = os.system(cmd) ``` `os.system()` invokes a command shell. Consequently, characters such as `;`, redirections, and `#` in `source` become shell syntax instead of literal filename bytes. The proof member retains the accepted prefix and `.db` suffix while placing `;printf ...;sleep 2;#` between them. The generated shell command runs `printf`, and the comment suppresses the remainder of the `cp` command. No upstream fixed release or fix commit was identified in the ticket or the tested repository state. The reproduction therefore labels its negative control as a **same-version patched control**, not as an upstream fixed version. That control changes only the unsafe copy block to `shutil.copy2(source, backup_path)`. It accepts the same request and copies the literal filename without interpreting its metacharacters, proving that the shell sink is causal. ## Reproduction Steps 1. Run `bundle/repro/reproduction_steps.sh` from any directory. The script resolves `PRUVA_ROOT`, uses `/pruva/project-cache/repo` when the prepared cache contract permits it, and otherwise falls back to `bundle/artifacts/fledge`. 2. The script checks out exact commit `f90ffc2047ee49a380ada98a59fcc2985bd6a943`, verifies the vulnerable source statements, and builds the real Fledge product in a pinned Ubuntu 22.04 base image. Compilation is bounded to two jobs. The runtime includes the normal rsyslog socket and starts Fledge through its shipped `bin/fledge start` flow. 3. It generates deterministic minimal archives at runtime and exercises: - one normal valid backup upload (marker must remain absent); - two fresh vulnerable service instances with distinct crafted archives and markers; - two fresh same-version patched-control instances with the exact corresponding archive bytes (requests succeed, literal files are copied, and markers remain absent). 4. Each service must become healthy at `/fledge/ping`, report version 3.1.0 with mandatory authentication, and accept a normal `admin` login before the upload. 5. The vulnerable requests must return HTTP 200, produce service debug logs showing the generated `cp` command, produce `strace` evidence of `/bin/sh -c`, show the shell as a child of the Fledge core process, and create markers owned by `fledge`. 6. Expected final output includes: ```text [+] CONFIRMED: authenticated remote OS command execution through Fledge 3.1.0 backup upload API [+] Vulnerable markers: PRUVA_CVE_2026_71284_ATTEMPT_1, PRUVA_CVE_2026_71284_ATTEMPT_2 [+] Benign and same-version patched controls reached the endpoint without marker execution ``` The final script was executed twice consecutively and returned exit status 0 on both runs. ## Evidence Current-run evidence is under `bundle/logs/repro/cve-2026-71284/`; `bundle/repro/runtime_manifest.json` binds every finalized artifact to its SHA-256. Key artifacts include: - `proof_summary.json` — concise result, exact commit and image identities, four fresh process/container identities, two successful vulnerable markers, and negative-control outcomes. - `source_identity.json` — repository, v3.1.0 commit, vulnerable file digest, and canonical source target digest. - `runtime_image_identity.txt` — pinned base, vulnerable image ID, patched-control image ID, and Docker inspection output. - `vulnerable_1_health_response.json` and `vulnerable_2_health_response.json` — real service health, version 3.1.0, and mandatory authentication. - `vulnerable_{1,2}_login_request.json` / `login_response.json` — normal administrator authentication with current tokens redacted from retained proof. - `vulnerable_{1,2}_upload_request.json` / `upload_response.json` — authenticated production endpoint transactions. - `vulnerable_{1,2}_service.log` — handler debug output containing attacker-controlled `source` and the constructed shell command. - `vulnerable_{1,2}_execve_trace.log` and `shell_lineage.log` — syscall/process evidence. A representative excerpt is: ```text execve("/bin/sh", ["sh", "-c", "cp /var/lib/fledge/upload/fledge_backup_...db;printf PRUVA_CVE_2026_71284_ATTEMPT_1 >/tmp/pruva_cve_2026_71284_attempt_1;sleep 2;#.db /var/lib/fledge/backup"], ...) ``` - `vulnerable_1_process_tree_during_attack.txt` — live lineage showing PID 155 `python3 -m fledge.services.core` (user `fledge`) parenting PID 579 `sh -c ...`, which parents `sleep 2`. - `vulnerable_{1,2}_marker.txt` — exact attacker-selected marker bytes. - `vulnerable_{1,2}_marker_stat.txt` — marker ownership and mode. Both record `uid=10001 user=fledge ... group=fledge`. - `benign_control_*` — a conventional valid backup succeeds without a marker and appears in the backup directory. - `patched_{1,2}_negative_control.json`, `patched_{1,2}_execve_trace.log`, `patched_{1,2}_service.log`, and `patched_{1,2}_backup_listing.txt` — the same crafted archive reaches the same endpoint in the same release, but no attacker command is passed to `execve`; the source is copied as a literal filename and no marker appears. - `patched_control_source.txt` — exact behavior-preserving control statement using `shutil.copy2`. - `bundle/logs/reproduction_steps.log` and `bundle/logs/fledge-image-build.log` — diagnostics for the final run; these are not hashed as immutable proof while active. The final runtime manifest records `entrypoint_kind="endpoint"`, `service_started=true`, `healthcheck_passed=true`, `target_path_reached=true`, exact source commit, source target digest, vulnerable runtime image digest, Linux/x86-64 platform, and 101 finalized proof artifacts. ## Recommendations / Next Steps 1. Remove the shell from the copy operation. Prefer `shutil.copy2(source, backup_path)` or another API that treats source and destination as literal path arguments. If an external utility is unavoidable, invoke it with an argument vector and `shell=False`; do not build a shell command string. 2. Validate and normalize every archive member before extraction. Reject absolute paths, `..` traversal, links, device entries, control characters, and filenames outside a narrowly defined grammar. Extract only explicitly accepted members rather than calling unrestricted `extractall()`. 3. Do not rely solely on `startswith()`/`endswith()` for a security boundary. A suitable allowlist should constrain the entire backup basename, for example a known timestamp pattern and the exact `.db`/`.dump` extension. 4. Add production-path tests that upload names containing `;`, `$()`, backticks, quotes, whitespace, redirection operators, glob characters, newlines, and traversal sequences. Assert no child shell is created and that literal-safe backups still work. 5. Add a regression test using the same crafted archive against the real authenticated API and monitor process ancestry (`Fledge core -> shell`) to ensure the unsafe behavior is gone. 6. Until an upstream release containing a verified fix exists, restrict the endpoint to trusted administrators, isolate the service account, minimize writable paths and privileges, and alert on shell processes spawned by the Fledge core service. ## Additional Notes - **Idempotency:** Confirmed. The script removes prior evidence/work directories and uniquely names containers; it completed successfully twice in succession. Each individual final run itself used two clean vulnerable and two clean patched-control service instances. - **Real boundary:** The proof uses the compiled Fledge 3.1.0 core and storage service, normal product initialization, `/fledge/login`, and `POST /fledge/backup/upload` over localhost TCP. It does not invoke `upload_backup()` directly and uses no mock handler. - **Sanitizers:** None used. The success oracle is real command execution, not a sanitizer or crash. - **Authentication:** Fledge's normal first-install administrator credential was used only inside the isolated containers. Retained request evidence redacts the credential and retained responses redact JWTs. - **Container capabilities:** `SYS_PTRACE` and an unconfined seccomp profile are used only so `strace` can capture child process execution. They do not enable the injection, change product parsing, or create the marker; vulnerable execution was independently observed before adding tracing. - **Upstream status:** No released fixed version is asserted. The negative control is explicitly source-modified Fledge 3.1.0 for causal verification. - **Limitations:** The proof requires a working Docker daemon and network access on an uncached first build to obtain the pinned base and package dependencies. It performs no outbound access from the exploit payload. ## Reproduction Details Reproduced: 2026-08-23T15:39:05.410Z Duration: 5463 seconds Tool calls: 502 Turns: Unknown Handoffs: 2 ## Quick Verification Run one of these commands to verify locally: pruva-verify REPRO-2026-00332 pruva-verify CVE-2026-71284 Or open in GitHub Codespaces (zero-friction, auto-runs): https://github.com/codespaces/new?ref=repro/REPRO-2026-00332&repo=N3mes1s/pruva-sandbox Or download and run the script manually: curl -O https://api.pruva.dev/v1/reproductions/REPRO-2026-00332/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-2026-71284 - Source: https://github.com/fledge-iot/fledge ## Artifacts - bundle/repro/rca_report.md (analysis, 11281 bytes) - bundle/repro/reproduction_steps.sh (reproduction_script, 30149 bytes) - bundle/logs/repro/cve-2026-71284/benign_control_backup_listing.txt (other, 37 bytes) - bundle/logs/repro/cve-2026-71284/benign_control_observation.json (other, 220 bytes) - bundle/logs/repro/cve-2026-71284/benign_control_upload_request.json (other, 353 bytes) - bundle/logs/repro/cve-2026-71284/benign_control_upload_response.headers (other, 158 bytes) - bundle/logs/repro/cve-2026-71284/benign_control_upload_response.json (other, 72 bytes) - bundle/logs/repro/cve-2026-71284/inputs/archive_sha256.txt (other, 439 bytes) - bundle/logs/repro/cve-2026-71284/inputs/attack_1_member_name.txt (other, 125 bytes) - bundle/logs/repro/cve-2026-71284/inputs/attack_2_member_name.txt (other, 125 bytes) - bundle/logs/repro/cve-2026-71284/inputs/fledge_backup_attack_1.tar.gz (other, 273 bytes) - bundle/logs/repro/cve-2026-71284/inputs/fledge_backup_attack_2.tar.gz (other, 273 bytes) - bundle/logs/repro/cve-2026-71284/inputs/fledge_backup_benign.tar.gz (other, 163 bytes) - bundle/logs/repro/cve-2026-71284/patched_1_backup_listing.txt (other, 44 bytes) - bundle/logs/repro/cve-2026-71284/patched_1_container_console.log (log, 30389 bytes) - bundle/logs/repro/cve-2026-71284/patched_1_container_final.json (other, 9887 bytes) - bundle/logs/repro/cve-2026-71284/patched_1_container_running.json (other, 10383 bytes) - bundle/logs/repro/cve-2026-71284/patched_1_execve_trace.log (log, 2596 bytes) - bundle/logs/repro/cve-2026-71284/patched_1_final_process_tree.txt (other, 687 bytes) - bundle/logs/repro/cve-2026-71284/patched_1_health_request.txt (other, 34 bytes) - bundle/logs/repro/cve-2026-71284/patched_1_health_response.json (other, 248 bytes) - bundle/logs/repro/cve-2026-71284/patched_1_logging_request.json (other, 92 bytes) - bundle/logs/repro/cve-2026-71284/patched_1_logging_response.json (other, 242 bytes) - bundle/logs/repro/cve-2026-71284/patched_1_login_request.json (other, 164 bytes) - bundle/logs/repro/cve-2026-71284/patched_1_login_response.headers (other, 159 bytes) - bundle/logs/repro/cve-2026-71284/patched_1_login_response.json (other, 115 bytes) - bundle/logs/repro/cve-2026-71284/patched_1_negative_control.json (other, 220 bytes) - bundle/logs/repro/cve-2026-71284/patched_1_runtime_identity.txt (other, 30531 bytes) - bundle/logs/repro/cve-2026-71284/patched_1_service.log (log, 17667 bytes) - bundle/logs/repro/cve-2026-71284/patched_1_strace_attach.log (log, 451 bytes) - bundle/logs/repro/cve-2026-71284/patched_1_upload_request.json (other, 355 bytes) - bundle/logs/repro/cve-2026-71284/patched_1_upload_response.headers (other, 158 bytes) - bundle/logs/repro/cve-2026-71284/patched_1_upload_response.json (other, 74 bytes) - bundle/logs/repro/cve-2026-71284/patched_2_backup_listing.txt (other, 44 bytes) - bundle/logs/repro/cve-2026-71284/patched_2_container_console.log (log, 30966 bytes) - bundle/logs/repro/cve-2026-71284/patched_2_container_final.json (other, 9886 bytes) - bundle/logs/repro/cve-2026-71284/patched_2_container_running.json (other, 10383 bytes) - bundle/logs/repro/cve-2026-71284/patched_2_execve_trace.log (log, 2631 bytes) - bundle/logs/repro/cve-2026-71284/patched_2_final_process_tree.txt (other, 687 bytes) - bundle/logs/repro/cve-2026-71284/patched_2_health_request.txt (other, 34 bytes) - bundle/logs/repro/cve-2026-71284/patched_2_health_response.json (other, 248 bytes) - bundle/logs/repro/cve-2026-71284/patched_2_logging_request.json (other, 92 bytes) - bundle/logs/repro/cve-2026-71284/patched_2_logging_response.json (other, 242 bytes) - bundle/logs/repro/cve-2026-71284/patched_2_login_request.json (other, 164 bytes) - bundle/logs/repro/cve-2026-71284/patched_2_login_response.headers (other, 159 bytes) - bundle/logs/repro/cve-2026-71284/patched_2_login_response.json (other, 115 bytes) - bundle/logs/repro/cve-2026-71284/patched_2_negative_control.json (other, 220 bytes) - bundle/logs/repro/cve-2026-71284/patched_2_runtime_identity.txt (other, 30874 bytes) - bundle/logs/repro/cve-2026-71284/patched_2_service.log (log, 17667 bytes) - bundle/logs/repro/cve-2026-71284/patched_2_strace_attach.log (log, 509 bytes) - bundle/logs/repro/cve-2026-71284/patched_2_upload_request.json (other, 355 bytes) - bundle/logs/repro/cve-2026-71284/patched_2_upload_response.headers (other, 158 bytes) - bundle/logs/repro/cve-2026-71284/patched_2_upload_response.json (other, 74 bytes) - bundle/logs/repro/cve-2026-71284/patched_control_source.txt (other, 514 bytes) - bundle/logs/repro/cve-2026-71284/proof_summary.json (other, 1254 bytes) - bundle/logs/repro/cve-2026-71284/runtime_image_identity.txt (other, 9246 bytes) - bundle/logs/repro/cve-2026-71284/source_identity.json (other, 334 bytes) - bundle/logs/repro/cve-2026-71284/vulnerable_1_capability_observation.json (other, 219 bytes) - bundle/logs/repro/cve-2026-71284/vulnerable_1_container_console.log (log, 6062 bytes) - bundle/logs/repro/cve-2026-71284/vulnerable_1_container_final.json (other, 9657 bytes) - bundle/logs/repro/cve-2026-71284/vulnerable_1_container_running.json (other, 10153 bytes) - bundle/logs/repro/cve-2026-71284/vulnerable_1_final_process_tree.txt (other, 620 bytes) - bundle/logs/repro/cve-2026-71284/vulnerable_1_health_request.txt (other, 34 bytes) - bundle/logs/repro/cve-2026-71284/vulnerable_1_health_response.json (other, 248 bytes) - bundle/logs/repro/cve-2026-71284/vulnerable_1_logging_request.json (other, 92 bytes) - bundle/logs/repro/cve-2026-71284/vulnerable_1_logging_response.json (other, 242 bytes) - bundle/logs/repro/cve-2026-71284/vulnerable_1_login_request.json (other, 164 bytes) - bundle/logs/repro/cve-2026-71284/vulnerable_1_login_response.headers (other, 159 bytes) - bundle/logs/repro/cve-2026-71284/vulnerable_1_login_response.json (other, 115 bytes) - bundle/logs/repro/cve-2026-71284/vulnerable_1_marker.txt (other, 30 bytes) - bundle/logs/repro/cve-2026-71284/vulnerable_1_marker_stat.txt (other, 101 bytes) - bundle/logs/repro/cve-2026-71284/vulnerable_1_runtime_identity.txt (other, 30581 bytes) - bundle/logs/repro/cve-2026-71284/vulnerable_1_service.log (log, 18590 bytes) - bundle/logs/repro/cve-2026-71284/vulnerable_1_shell_lineage.log (log, 261 bytes) - bundle/logs/repro/cve-2026-71284/vulnerable_1_strace_attach.log (log, 596 bytes) - bundle/logs/repro/cve-2026-71284/vulnerable_1_upload_response.headers (other, 158 bytes) - bundle/logs/repro/cve-2026-71284/vulnerable_1_upload_response.json (other, 74 bytes) - bundle/logs/repro/cve-2026-71284/vulnerable_2_capability_observation.json (other, 219 bytes) - bundle/logs/repro/cve-2026-71284/vulnerable_2_container_console.log (log, 31932 bytes) - bundle/logs/repro/cve-2026-71284/vulnerable_2_container_final.json (other, 9657 bytes) - bundle/logs/repro/cve-2026-71284/vulnerable_2_container_running.json (other, 10153 bytes) - bundle/logs/repro/cve-2026-71284/vulnerable_2_final_process_tree.txt (other, 687 bytes) - bundle/logs/repro/cve-2026-71284/vulnerable_2_health_request.txt (other, 34 bytes) - bundle/logs/repro/cve-2026-71284/vulnerable_2_health_response.json (other, 248 bytes) - bundle/logs/repro/cve-2026-71284/vulnerable_2_logging_request.json (other, 92 bytes) - bundle/logs/repro/cve-2026-71284/vulnerable_2_logging_response.json (other, 242 bytes) - bundle/logs/repro/cve-2026-71284/vulnerable_2_login_request.json (other, 164 bytes) - bundle/logs/repro/cve-2026-71284/vulnerable_2_login_response.headers (other, 159 bytes) - bundle/logs/repro/cve-2026-71284/vulnerable_2_login_response.json (other, 115 bytes) - bundle/logs/repro/cve-2026-71284/vulnerable_2_marker.txt (other, 30 bytes) - bundle/logs/repro/cve-2026-71284/vulnerable_2_marker_stat.txt (other, 101 bytes) - bundle/logs/repro/cve-2026-71284/vulnerable_2_process_tree_during_attack.txt (other, 2372 bytes) - bundle/logs/repro/cve-2026-71284/vulnerable_2_runtime_identity.txt (other, 30776 bytes) - bundle/logs/repro/cve-2026-71284/vulnerable_2_service.log (log, 18876 bytes) - bundle/logs/repro/cve-2026-71284/vulnerable_2_shell_lineage.log (log, 261 bytes) - bundle/logs/repro/cve-2026-71284/vulnerable_2_strace_attach.log (log, 596 bytes) - bundle/logs/repro/cve-2026-71284/vulnerable_2_upload_request.json (other, 355 bytes) - bundle/logs/repro/cve-2026-71284/vulnerable_2_upload_response.headers (other, 158 bytes) - bundle/logs/repro/cve-2026-71284/vulnerable_2_upload_response.json (other, 74 bytes) - bundle/repro/runtime_manifest.json (other, 20924 bytes) - bundle/repro/validation_verdict.json (other, 869 bytes) ## API Access - JSON: https://api.pruva.dev/v1/reproductions/REPRO-2026-00332 - Script: https://api.pruva.dev/v1/reproductions/REPRO-2026-00332/artifacts/bundle/repro/reproduction_steps.sh - Web: https://www.pruva.dev/reproductions/REPRO-2026-00332 ## 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://www.pruva.dev