CVE-2026-58426: Gitea Actions artifact signed URL context confusion
I reported a Gitea Actions Artifacts V4 signed URL issue that was published as CVE-2026-58426 / GHSA-hg5r-vq93-9fv6.
The issue affected Gitea’s V4 artifact UploadArtifact and DownloadArtifact signed URL handlers. An attacker who could run a Gitea Actions job could obtain a valid signed URL for an attacker-controlled artifact, rewrite selected URL fields while preserving the original HMAC input, and make the unauthenticated signed URL endpoint operate in another task and repository context.
Advisory
- CVE:
CVE-2026-58426 - GitHub Advisory:
GHSA-hg5r-vq93-9fv6 - Package:
code.gitea.io/gitea - Ecosystem: Go module
- Affected versions:
>= 1.22.0, <= 1.26.1 - Patched version:
1.26.2 - Severity: Critical 9.6/10
- CVSS:
CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:C/C:H/I:H/A:N - CWE:
CWE-347: Improper Verification of Cryptographic Signature
Preconditions
The vulnerable deployment shape required all of the following:
- Gitea Actions was enabled;
- V4 artifact signed URL handling was in use;
- the attacker could run an Actions job and create an artifact in an attacker-controlled task;
- the target task was still running during the signed URL validity window;
- the target task and artifact identifiers or names were known or predictable enough to construct a matching URL tuple.
The issue applies to the path where Gitea’s own V4 UploadArtifact or DownloadArtifact signed URL handlers process the final URL. Deployments that hand clients a direct storage backend URL need separate assessment because that may bypass the affected Gitea handlers.
Root Cause
The root cause was an ambiguous HMAC payload.
The V4 artifact signed URL signature was built by writing several values into the HMAC one after another:
- endpoint;
- expiry;
- artifact name;
- task ID;
- artifact ID.
Those fields were not encoded as a structured payload, length-prefixed, or otherwise separated in a way that made the tuple unambiguous. That meant two different URL tuples could produce the same byte stream before signing.
For example, these two simplified suffixes collapse to the same HMAC input:
1
2
artifactName = "build-12", taskID = 34, artifactID = 56
artifactName = "build-1", taskID = 23, artifactID = 456
Both serialize as:
1
build-123456
That ambiguity mattered because the final signed URL endpoints were unauthenticated by design. The signed URL itself was the bearer authorization token, so the HMAC needed to bind the URL to exactly one artifact, task, run, repository, endpoint, and expiry.
After signature verification, the vulnerable path trusted URL-selected context. The verifier loaded the task identified by the URL and checked that it was running. It parsed the artifact ID for signature verification, but the later artifact lookup used the URL-selected task/run context and artifact name rather than binding the request to the signed artifact ID as the authoritative object.
Attack Shape
The attacker first creates an artifact in a task they control. The artifact name is chosen so that the final HMAC input can be reinterpreted as a different tuple targeting another running task.
The attacker then obtains a legitimate signed URL for their own artifact and rewrites fields such as:
artifactName;taskID;artifactID.
Because the raw HMAC input remains unchanged, the signature still verifies. The post-verification code then uses the rewritten task and artifact name to operate in the target context.
Two exploit paths were relevant:
DownloadArtifact: a forged finalGETcould read an artifact from a target running task, including in a private repository;UploadArtifact: a forged finalPUTcould append attacker-controlled bytes into the target artifact upload staging context and mutate artifact metadata.
The attacker did not need permission to ask the authenticated Actions API for a target-run signed URL. The bypass happened after a valid attacker-owned URL was minted and then rewritten.
Impact
The impact is a cross-task and cross-repository authorization bypass in Gitea Actions artifact handling.
Confirmed impact included:
- reading artifacts from a target running task in a private repository;
- writing attacker-controlled bytes into a target artifact’s upload staging storage;
- changing target artifact metadata during the forged upload path.
This can expose build outputs, packaged artifacts, logs, or workflow-generated files. On the integrity side, the primitive can interfere with in-flight artifact upload and staging state. Depending on the target workflow and finalization path, that may affect later workflow steps, release automation, deployment jobs, or downstream users consuming the artifact.
Why This Was Easy To Miss
The dangerous part was not that the signed URL handlers were unauthenticated. That is expected for bearer-style signed URLs. The dangerous part was that the signed data was not canonical enough to describe exactly one security context.
HMAC usage can create a false sense of safety when the signed bytes are ambiguous. The signature proved that someone signed a byte string, but it did not prove which logical tuple the server should associate with that byte string.
There was also a second trap: the artifactID looked signed, but it was not used as the authoritative artifact object after verification. Once the server loaded a task and looked up the artifact by the rewritten task/run/name context, the signed URL could drift away from the artifact it was originally minted for.
Fix Direction
The fix needs to bind signed artifact URLs to one canonical security context.
Defensive principles:
- sign a structured, versioned payload rather than raw concatenated fields;
- include endpoint, expiry, task ID, artifact ID, artifact name, run ID, run attempt ID, repository ID, and owner ID in the signed payload;
- after signature verification, load the artifact by the signed artifact ID;
- reject the request unless the signed artifact ID, task, run, repository, owner, artifact name, endpoint, and expiry all match the request context;
- add regression tests for tuple-boundary collisions and cross-task signed URL rewrites;
- validate V4 artifact names as defense in depth.
Adding separators alone is not enough if the post-verification path still trusts URL fields that are not bound to the loaded artifact. The important invariant is that a signed URL authorizes one specific artifact operation in one specific repository/task context.
References
- GitHub Advisory: https://github.com/go-gitea/gitea/security/advisories/GHSA-hg5r-vq93-9fv6
- Gitea project: https://github.com/go-gitea/gitea