# Gitea Actions Artifacts V4 signed URL HMAC ambiguity allows cross-repository artifact read and cross-task upload-state write

Gitea Actions V4 artifact signed URLs HMAC raw concatenated fields without delimiters, letting an Actions runner rewrite taskID/artifactName/artifactID to read private-repo artifacts and write into another task's staging.

- Payout: Research
- Severity: critical
- Program: Gitea
- Found by (vendor credit): kamil-sawicki
- Found by: kamil-sawicki
- Disclosed: 2026-07-21
- CVE: CVE-2026-58426
- Write-up quality: 9/10
- Original: https://github.com/advisories/GHSA-hg5r-vq93-9fv6
- Library page: https://bugbountylibrary.com/r/ghsa%3AGHSA-hg5r-vq93-9fv6
- Tags: area:web domain:web impact:data-leak impact:integrity kind:research platform:ghsa sev:critical tech:go technique:source-review vuln:access-control vuln:auth-bypass vuln:crypto vuln:info-disclosure

**Target:** Gitea Actions Artifacts V4 signed upload/download URL handlers
**Root cause:** buildSignature HMACs endpoint||expires||artifactName||taskID||artifactID with no delimiters or length prefixes, so distinct tuples collide; after verification the handler trusts URL-controlled taskID/artifactName and looks up the artifact by run/name instead of the signed artifactID.
**Steps:**
1. Obtain a legitimate V4 signed URL for an attacker-controlled artifact named `artifact-795-153` on attacker task 48.
2. Rewrite the URL to `artifactName=artifact-795-1`, `taskID=53`, `artifactID=48<attackerArtifactID>` so the HMAC input suffix `artifact-795-15348<attackerArtifactID>` is unchanged.
3. Send the forged final `GET` with no token to DownloadArtifact; server resolves target task 53's run/attempt and serves `artifact-795-1` from private `user2/repo2`.
4. Rewrite the signed upload URL identically and send `PUT ...&comp=appendBlock` with attacker data to write into the target run/artifact staging path and mutate target artifact metadata.
**How found:** Code review of buildSignature noticed raw concatenation of variable-length fields without separators, then observed verifySignature trusts URL taskID/artifactName and getArtifactByName looks up by run/name rather than the signed artifactID, enabling tuple-boundary collision.
**Exploitation:** Attacker with permission to run a Gitea Actions job creates and finalizes their own V4 artifact to get a valid signed URL, then rewrites taskID/artifactName/artifactID preserving the HMAC. Unauthenticated forged GET reads a private-repo artifact from another running task; forged PUT appends attacker bytes to the target artifact staging storage and updates its metadata, poisoning data later consumed by FinalizeArtifact.
**Escalation:** Single signed URL yields both cross-repository private artifact read (C:H) and cross-task staging write/metadata mutation (I:H); staged bytes are later consumed by FinalizeArtifact, potentially poisoning release/deploy workflows.
**Insight:** When an HMAC covers concatenated variable-length fields, look for boundary-shift collisions between adjacent fields (name+taskID+artifactID); also check whether post-verification code re-derives context from URL params instead of the signed identifier, which turns a signature collision into full context confusion.

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

**Forged signed URL tuple rewrite**

```
artifactName=artifact-795-1
taskID=53
artifactID=48<attackerArtifactID>
```

**PoC run command**

```
cd /home/kali/gitea/pocs
GITEA_REPO=/home/kali/gitea/repo GOTOOLCHAIN=auto go run ./actions_artifact_v4_cross_task_access_dynamic_poc.go
```

**HMAC input collision example**

```
signed tuple:
artifactName = "artifact-795-153"
taskID       = 48
artifactID   = <attacker artifact id>

forged tuple:
artifactName = "artifact-795-1"
taskID       = 53
artifactID   = 48<attacker artifact id>

The final HMAC input suffix is identical:

artifact-795-15348<attacker artifact id>
```

