{"repro_id":"REPRO-2026-00283","version":6,"title":"Nuclio cron trigger shell command injection leading to RCE","repro_type":"security","status":"published","severity":"critical","description":"Nuclio controller builds curl invocation strings for Kubernetes CronJob-based cron triggers and stores them as /bin/sh -c command arguments without adequate sanitization. Malicious event.headers keys can break quoting in --header arguments, and event.body can leverage shell command substitution, resulting in arbitrary OS command execution in the cluster. Affected versions include Nuclio 1.15.27 and earlier, with the documented fix at the post-20260601075854 build referenced by advisories.","root_cause":"# Root Cause Analysis: CVE-2026-52831 Nuclio Cron Trigger Shell Command Injection\n\n## Summary\n\nNuclio's Kubernetes controller generated Kubernetes CronJob containers for cron triggers by concatenating attacker-controlled cron event data into a single shell command and storing it as `Args: [\"/bin/sh\", \"-c\", curlCommand]`. A malicious `event.headers` key containing an unescaped double quote and shell separators breaks out of the intended `curl --header \"key: value\"` quoting and appends arbitrary shell commands. In this run, a real Nuclio controller binary processed a real `NuclioFunction` Kubernetes custom resource, generated a real Kubernetes CronJob, and a CronJob-derived pod executed attacker-controlled `echo` and `id` commands.\n\n## Impact\n\n- **Package/component affected:** Nuclio Kubernetes controller, specifically Kubernetes CronJob-based cron trigger generation in `pkg/platform/kube/functionres/lazy.go`.\n- **Affected versions:** Nuclio versions at or before the vulnerable parent of fixed commit `3356b86a8bfab3f960aa420310ebff765df9dede`; the ticket states Nuclio 1.15.27 and earlier are affected.\n- **Risk level and consequences:** Critical. A user who can create or update a Nuclio function with a malicious cron trigger can cause the cluster to create a CronJob whose pod runs arbitrary shell commands in the CronJob container context. This is product-path remote/API-triggered command execution via the Kubernetes/Nuclio API boundary.\n\n## Impact Parity\n\n- **Disclosed/claimed maximum impact:** Code execution / RCE through Nuclio cron trigger event headers or body in Kubernetes CronJob-based cron trigger processing.\n- **Reproduced impact from this run:** Code execution in real Kubernetes CronJob pods generated by the Nuclio controller. The vulnerable pod logs include the attacker marker `CVE_PRODUCT_RCE` and `id` output (`uid=100(curl_user) ...`).\n- **Parity:** `full`\n- **Not demonstrated:** No attempt was made to chain from CronJob-container command execution to broader cluster privilege escalation. The reproduced impact matches the claimed command execution at the affected product boundary.\n\n## Root Cause\n\nIn the vulnerable code path, Nuclio decoded cron trigger attributes from a `NuclioFunction` resource and built a shell command string for invoking the function:\n\n- User-controlled headers were formatted as `--header \"<headerKey>: <headerValue>\"` and appended into one string.\n- The final curl invocation was embedded in `Args: []string{\"/bin/sh\", \"-c\", curlCommand}` for the CronJob container.\n- Because header keys were not escaped for shell context, a key such as `X-Exploit\"; echo CVE_PRODUCT_RCE; id; echo \"` terminates the intended quoted header and injects shell commands.\n- The vulnerable parent commit used `/bin/sh -c`; the fixed commit switches to exec-form invocation, `Command: []string{\"curl\"}`, passes each curl argument as a separate argv entry, and uses `--data-raw` for the body so no shell interprets attacker data.\n\nThe fixed commit used for negative control is `3356b86a8bfab3f960aa420310ebff765df9dede` (`[Security] Fix cron trigger shell injection`). The reproduction script checks out and builds `3356b86a8bfa^` for the vulnerable path and `3356b86a8bfa` for the fixed path, and verifies the relevant patch hunk before running.\n\n## Reproduction Steps\n\n1. Use `bundle/repro/reproduction_steps.sh`.\n2. The script:\n   - Reuses the prepared Nuclio repository cache when available.\n   - Resolves the vulnerable commit as `3356b86a8bfa^` and the fixed commit as `3356b86a8bfa`.\n   - Builds the real Nuclio `cmd/controller` binary for both commits.\n   - Creates fresh kind Kubernetes clusters for two vulnerable attempts and two fixed attempts.\n   - Starts the real Nuclio controller binary inside each kind control-plane container.\n   - Applies real `NuclioFunction` and `NuclioProject` custom resources.\n   - Waits for the controller to generate a Kubernetes CronJob from the malicious cron trigger.\n   - Creates a manual Job from that CronJob and captures the real pod logs.\n3. Expected evidence:\n   - Vulnerable attempts: CronJob JSON contains `args[0] == \"/bin/sh\"` and `args[1] == \"-c\"`, and pod logs contain `CVE_PRODUCT_RCE` and `uid=`.\n   - Fixed attempts: CronJob JSON contains `command: [\"curl\"]`, does not contain `/bin/sh` or `-c`, and pod logs contain only the stub HTTP response `ok` with no attacker marker.\n\n## Evidence\n\n- `bundle/logs/reproduction_steps.log` — top-level run log for the final successful run.\n- `bundle/repro/runtime_manifest.json` — structured runtime manifest showing `entrypoint_kind=\"endpoint\"`, `service_started=true`, `healthcheck_passed=true`, and `target_path_reached=true`.\n- `bundle/logs/vulnerable_attempt1/cronjob_pretty.json` and `bundle/logs/vulnerable_attempt2/cronjob_pretty.json` — vulnerable controller-generated CronJobs. They show the generated container args include `/bin/sh`, `-c`, and an injected shell command string containing `echo CVE_PRODUCT_RCE; id`.\n- `bundle/logs/vulnerable_attempt1/pod.log` and `bundle/logs/vulnerable_attempt2/pod.log` — real CronJob-derived pod logs. Key excerpt:\n\n```text\nCVE_PRODUCT_RCE\nuid=100(curl_user) gid=101(curl_group) groups=101(curl_group)\n```\n\n- `bundle/logs/fixed_attempt1/cronjob_pretty.json` and `bundle/logs/fixed_attempt2/cronjob_pretty.json` — fixed controller-generated CronJobs. They show `command: [\"curl\"]` and discrete args containing the malicious strings as data, not shell syntax.\n- `bundle/logs/fixed_attempt1/pod.log` and `bundle/logs/fixed_attempt2/pod.log` — fixed negative control pod logs. They contain `ok` and do not contain `CVE_FIXED_SHOULD_NOT_RUN`.\n- Environment details captured in `bundle/logs/reproduction_steps.log` include Go, Docker, kind, resolved commits, and built controller binary metadata.\n\n## Recommendations / Next Steps\n\n- Keep the fixed exec-form approach: never pass user-controlled cron trigger header keys, header values, or body through `/bin/sh -c`.\n- Pass curl and all arguments as a command/argv vector, not a shell command string.\n- Use `--data-raw` or an equivalent safe body handling mechanism to avoid file expansion or shell substitution primitives.\n- Add regression tests that exercise malicious header keys, header values, and bodies through the controller/CronJob generation path and assert no shell is present in the resulting pod spec.\n- Upgrade Nuclio deployments to a version containing fixed commit `3356b86a8bfab3f960aa420310ebff765df9dede` or later.\n\n## Additional Notes\n\n- Idempotency confirmation: `bundle/repro/reproduction_steps.sh` was run successfully twice consecutively after the final fixes.\n- The script uses real Kubernetes objects and a real Nuclio controller binary, but it starts the controller directly in the kind control-plane container rather than installing the full Helm chart. This preserves the relevant production boundary: Kubernetes API/CRD watch -> Nuclio controller reconciliation -> generated Kubernetes CronJob -> real CronJob pod execution.\n- A small busybox HTTP stub is deployed only so fixed exec-form curl has a reachable function service target and can complete without retry timeout. It is not used to simulate the vulnerable controller or CronJob generation behavior.\n","ghsa_id":"GHSA-v5px-423j-pf7p","cve_id":"CVE-2026-52831","cwe_id":"CWE-78 (Improper Neutralization of Special Elements used in an OS Command)","source_url":"https://github.com/nuclio/nuclio","package":{"name":"github.com/nuclio/nuclio","ecosystem":"go","affected_versions":"<= 1.15.27 (all versions before 0.0.0-20260601075854-3356b86a8bfa)"},"reproduced_at":"2026-07-11T13:13:12.877580+00:00","duration_secs":5340.0,"tool_calls":384,"handoffs":3,"total_cost_usd":19.483415130000015,"agent_costs":{"hypothesis_generator":0.0465408,"judge":1.049667,"repro":16.09657655,"support":0.09853978,"vuln_variant":2.1920910000000005},"cost_breakdown":{"hypothesis_generator":{"accounts/fireworks/models/kimi-k2p7-code":0.0465408},"judge":{"gpt-5.5":1.049667},"repro":{"accounts/fireworks/models/kimi-k2p7-code":1.8818965499999991,"gpt-5.5":14.214679999999998},"support":{"accounts/fireworks/models/kimi-k2p7-code":0.09853978},"vuln_variant":{"gpt-5.5":2.1920910000000005}},"quality":{"confidence":"high","idempotent_verified":false,"community_verifications":0},"environment":{"sandbox_image":"ghcr.io/n3mes1s/pruva-sandbox@sha256:8096b2518d6022e13d68f885c3b8ded6b4fe607098b1a1ccbfb99abc004d1dc1"},"published_at":"2026-07-11T13:13:40.318330+00:00","retracted":false,"artifacts":[{"path":"bundle/repro/reproduction_steps.sh","filename":"reproduction_steps.sh","size":20808,"category":"reproduction_script"},{"path":"bundle/repro/rca_report.md","filename":"rca_report.md","size":7239,"category":"analysis"},{"path":"bundle/vuln_variant/reproduction_steps.sh","filename":"reproduction_steps.sh","size":9267,"category":"reproduction_script"},{"path":"bundle/vuln_variant/rca_report.md","filename":"rca_report.md","size":8712,"category":"analysis"},{"path":"bundle/artifact_promotion_manifest.json","filename":"artifact_promotion_manifest.json","size":10914,"category":"other"},{"path":"bundle/artifact_promotion_report.json","filename":"artifact_promotion_report.json","size":10932,"category":"other"},{"path":"bundle/vuln_variant/source_identity.json","filename":"source_identity.json","size":934,"category":"other"},{"path":"bundle/vuln_variant/root_cause_equivalence.json","filename":"root_cause_equivalence.json","size":1308,"category":"other"},{"path":"bundle/logs/vulnerable_test.log","filename":"vulnerable_test.log","size":4060,"category":"log"},{"path":"bundle/logs/fixed_test.log","filename":"fixed_test.log","size":23341,"category":"log"},{"path":"bundle/repro/validation_verdict.json","filename":"validation_verdict.json","size":752,"category":"other"},{"path":"bundle/logs/reproduction_steps.log","filename":"reproduction_steps.log","size":9692,"category":"log"},{"path":"bundle/logs/vulnerable_attempt1/pod.log","filename":"pod.log","size":458,"category":"log"},{"path":"bundle/logs/fixed_attempt1/cronjob_pretty.json","filename":"cronjob_pretty.json","size":3457,"category":"other"},{"path":"bundle/repro/runtime_manifest.json","filename":"runtime_manifest.json","size":1099,"category":"other"},{"path":"bundle/logs/vulnerable_attempt2/pod.log","filename":"pod.log","size":458,"category":"log"},{"path":"bundle/logs/vulnerable_attempt1/cronjob_pretty.json","filename":"cronjob_pretty.json","size":3087,"category":"other"},{"path":"bundle/logs/fixed_attempt1/pod.log","filename":"pod.log","size":2,"category":"log"},{"path":"bundle/logs/fixed_attempt2/pod.log","filename":"pod.log","size":2,"category":"log"},{"path":"bundle/vuln_variant/patch_analysis.md","filename":"patch_analysis.md","size":5674,"category":"documentation"},{"path":"bundle/vuln_variant/variant_manifest.json","filename":"variant_manifest.json","size":3544,"category":"other"},{"path":"bundle/vuln_variant/validation_verdict.json","filename":"validation_verdict.json","size":2793,"category":"other"},{"path":"bundle/logs/vuln_variant/reproduction_steps.log","filename":"reproduction_steps.log","size":4343,"category":"log"},{"path":"bundle/vuln_variant/runtime_manifest.json","filename":"runtime_manifest.json","size":1376,"category":"other"},{"path":"bundle/logs/vuln_variant/source_identity.txt","filename":"source_identity.txt","size":402,"category":"other"},{"path":"bundle/logs/vuln_variant/patch_diff.stat","filename":"patch_diff.stat","size":258,"category":"other"},{"path":"bundle/logs/vuln_variant/lazy_vulnerable_excerpt.txt","filename":"lazy_vulnerable_excerpt.txt","size":3347,"category":"other"},{"path":"bundle/logs/vuln_variant/lazy_fixed_excerpt.txt","filename":"lazy_fixed_excerpt.txt","size":3533,"category":"other"},{"path":"bundle/logs/vuln_variant/security_policy_scan.txt","filename":"security_policy_scan.txt","size":766,"category":"other"},{"path":"bundle/logs/vuln_variant/candidate_matrix.log","filename":"candidate_matrix.log","size":1697,"category":"log"},{"path":"bundle/logs/vuln_variant/curl_header_at_probe.log","filename":"curl_header_at_probe.log","size":482,"category":"log"}]}