CVE-2026-58426
Gitea Actions Artifacts V4 signed URL HMAC ambiguity allows cross-repository artifact read and cross-task upload-state write
Summary
### Summary Gitea Actions Artifacts V4 signed upload/download URLs can be rewritten to access a different running task and repository context while preserving the original HMAC signature. An attacker with permission to run a Gitea Actions job can turn a signed URL for an attacker-controlled artifact into a URL that reads artifacts from another task context, or writes attacker-controlled data into another task's artifact upload staging context, including in a private repository. This is one vulnerability with two exploit paths: - `DownloadArtifact`: cross-task/cross-repository artifact read, giving `C:H`. - `UploadArtifact`: cross-task artifact staging write and metadata mutation, giving `I:H`. ### Details The root cause is that the V4 artifact signed URL signature is built from raw concatenated fields without delimiters or length-prefixing: ```go func (r *artifactV4Routes) buildSignature(endpoint, expires, artifactName string, taskID, artifactID int64) []byte { mac := hmac.New(sha256.New, setting.GetGeneralTokenSigningSecret()) mac.Write([]byte(endpoint)) mac.Write([]byte(expires)) mac.Write([]byte(artifactName)) _, _ = fmt.Fprint(mac, taskID) _, _ = fmt.Fprint(mac, artifactID) return mac.Sum(nil) } ``` Affected code: `routers/api/actions/artifactsv4.go:164-171`. Because `artifactName`, `taskID`, and `artifactID` are concatenated without boundaries, two different URL tuples can produce the same HMAC input. For example: ```text signed tuple: artifactName = "artifact-795-153" taskID = 48 artifactID = forged tuple: artifactName = "artifact-795-1" taskID = 53 artifactID = 48 ``` The final HMAC input suffix is identical: ```text artifact-795-15348 ``` The attacker does not need to know the target artifact's database `artifactID`. The forged URL's `artifactID` only needs to carry digits that preserve the original HMAC input. After verification, the actual target artifact is looked up by target task/run/attempt and `artifactName`, not by the signed `artifactID`. The signed URL handlers are unauthenticated and use `ArtifactV4Contexter()` only: ```go m.Group("", func() { m.Put("UploadArtifact", r.uploadArtifact) m.Get("DownloadArtifact", r.downloadArtifact) }, ArtifactV4Contexter()) ``` Affected code: `routers/api/actions/artifactsv4.go:156-159`. After verifying the HMAC, `verifySignature()` trusts the URL-controlled `taskID`, loads that task, checks that it is running, and returns the URL-controlled `artifactName`. It parses `artifactID` for the HMAC, but does not load or bind the artifact by that signed artifact ID: ```go task, err := actions_model.GetTaskByID(ctx, taskID) ... if task.Status != actions_model.StatusRunning { ... } ... return task, artifactName, true ``` Affected code: `routers/api/actions/artifactsv4.go:224-267`. The artifact lookup then uses the URL-selected task's run/attempt plus URL-selected artifact name: ```go has, err := db.GetEngine(ctx).Where(builder.Eq{ "run_id": runID, "run_attempt_id": runAttemptID, "artifact_name": name, }, builder.Like{"content_encoding", "%/%"}).Get(&art) ``` Affected code: `routers/api/actions/artifactsv4.go:270-278`. For download, the forged URL reaches `downloadArtifact()`, which verifies the signature, resolves the artifact by the forged task/run/name context, and serves it: ```go task, artifactName, ok := r.verifySignature(ctx, "DownloadArtifact") ... artifact, err := r.getArtifactByName(ctx, task.Job.RunID, task.Job.RunAttemptID, artifactName) ... err = actions.DownloadArtifactV4ReadStorage(ctx.Base, artifact) ``` Affected code: `routers/api/actions/artifactsv4.go:674-693`. For upload, the forged URL reaches `uploadArtifact()`, which verifies the signature, resolves the target artifact by the forged task/run/name context, appends attacker-controlled data, and updates targ
Affected packages
| Ecosystem | Package | Affected versions | Fixed versions |
|---|---|---|---|
| Go | code.gitea.io/gitea | — | 1.26.2 |
Remediation: Upgrade to 1.26.2 or later.
References
Includes data from the GitHub Advisory Database, licensed under CC-BY 4.0.
CVE® is a registered trademark of The MITRE Corporation. CVE content reproduced under the CVE Terms of Use; copyright designation © MITRE.
EPSS scores provided by the FIRST.org Exploit Prediction Scoring System.