GHSA-25GQ-J9JX-43PG

Vulnerability from github – Published: 2026-07-21 20:18 – Updated: 2026-07-21 20:18
VLAI
Summary
Gitea: Release attachment extension allowlist bypass via web release edit form (variant of CVE-2025-68939)
Details

Summary

The web handler EditReleasePost (routers/web/repo/release.go) reads form fields with prefix attachment-edit-{uuid} into a map[uuid]newName, passes that map to release_service.UpdateRelease, which writes the new name to the database via repo_model.UpdateAttachmentByUUID WITHOUT calling upload.Verify against setting.Repository.Release.AllowedTypes. The parent CVE-2025-68939 fix (PR #32151) added the equivalent upload.Verify call on the API edit endpoints via attachment_service.UpdateAttachment. The web release edit path was not updated.

A user with repository write permission can rename any existing release attachment to a name with a forbidden extension via the web release edit form, bypassing the operator-configured allowlist.

Details

Vulnerable code

routers/web/repo/release.go:597 EditReleasePost:

const editPrefix = "attachment-edit-"
editAttachments := make(map[string]string)
if setting.Attachment.Enabled {
    for k, v := range ctx.Req.Form {
        if strings.HasPrefix(k, editPrefix) {
            editAttachments[k[len(editPrefix):]] = v[0]
        }
    }
}
...
if err = release_service.UpdateRelease(ctx, ctx.Doer, ctx.Repo.GitRepo,
    rel, addAttachmentUUIDs, delAttachmentUUIDs, editAttachments); err != nil {
    ctx.ServerError("UpdateRelease", err)
    return
}

services/release/release.go:321 -- the unvalidated write:

for uuid, newName := range editAttachments {
    if !deletedUUIDs.Contains(uuid) {
        if err = repo_model.UpdateAttachmentByUUID(ctx, &repo_model.Attachment{
            UUID: uuid,
            Name: newName,
        }, "name"); err != nil {
            return err
        }
    }
}

No upload.Verify(nil, newName, setting.Repository.Release.AllowedTypes) before the database write.

Comparison: the parent fix on the API path

routers/api/v1/repo/release_attachment.go:341 (patched in PR #32151):

if err := attachment_service.UpdateAttachment(ctx,
        setting.Repository.Release.AllowedTypes, attach); err != nil {
    if upload.IsErrFileTypeForbidden(err) {
        ctx.Error(http.StatusUnprocessableEntity, "", err)
        return
    }
    ctx.Error(http.StatusInternalServerError, "UpdateAttachment", attach)
    return
}

Delegates to:

// services/attachment/attachment.go:96
func UpdateAttachment(ctx context.Context, allowedTypes string, attach *repo_model.Attachment) error {
    if err := upload.Verify(nil, attach.Name, allowedTypes); err != nil {
        return err
    }
    return repo_model.UpdateAttachment(ctx, attach)
}

The API path goes through attachment_service.UpdateAttachment which calls upload.Verify(nil, attach.Name, allowedTypes). The web path bypasses this entirely.

Proof of Concept

Tested live against: * Gitea v1.26.1 community edition, Linux amd64, SQLite, Go 1.26.2 * app.ini includes [repository.release] ALLOWED_TYPES = .zip,.tar.gz * Two users: admin (superuser, created via gitea admin user create --admin), bob (regular, repo owner of bob/test-repo)

Step 1: bob creates release v0.1 and uploads innocent.zip (allowlist compliant) via the API.

Step 2: Sanity. The patched API edit endpoint rejects a rename to a forbidden extension.

PATCH /api/v1/repos/bob/test-repo/releases/1/assets/1 HTTP/1.1
Authorization: token <bob_token>
Content-Type: application/json

{"name":"evil.exe"}

Response: HTTP 422 -- "This file cannot be uploaded or modified due to a forbidden file extension or type." (parent CVE-2025-68939 fix in action).

Step 3: The attack. The web release edit form does NOT enforce the allowlist.

POST /bob/test-repo/releases/edit/v0.1 HTTP/1.1
Cookie: i_like_gitea=<session>; lang=en-US
Content-Type: application/x-www-form-urlencoded

tag_name=v0.1
&tag_target=main
&title=rename+payload
&content=
&attachment-edit-<existing_attachment_uuid>=evil.exe

Response: HTTP 303 -> /bob/test-repo/releases. The form is accepted with no validation error.

Step 4: Verify.

GET /api/v1/repos/bob/test-repo/releases/1/assets/1 HTTP/1.1

Response includes "name": "evil.exe". The download link /attachments/<uuid> now serves the file under the forbidden extension.

A self contained Python PoC ships with this advisory: GITEA-R007_release_edit_extension_bypass.py. End to end run:

GITEA-R007_release_edit_extension_bypass.py

[+] Logged in as bob
[+] Pre-attack attachment name: 'innocent2.zip'
[+] API endpoint correctly rejects rename: HTTP 422 (parent CVE-2025-68939 fix)
[+] POST release edit: HTTP 303 -> /bob/test-repo/releases
[+] Post-attack attachment name: 'pwn.exe'

[!!!] CONFIRMED: web release edit bypasses Release.AllowedTypes allowlist.

Impact

Same impact class as the parent CVE-2025-68939 (HIGH, CVSS 8.2):

  • Pre-condition: operator has set Repository.Release.AllowedTypes to a non-empty allowlist (a reasonable hardening posture when restricting release uploads).
  • Threat actor: user holding repository write permission. In most Gitea deployments this is the repo owner, organization members, or invited collaborators.
  • Effect: bypass the allowlist; an attachment uploaded under an allowed extension is renamed to a forbidden extension (.exe, .html, .svg, .js, ...) and served by Gitea under that name.
  • Practical impact:
  • Distribute malware files (e.g., .exe, .dmg, .msi, .apk) masquerading as a tagged release attachment
  • If Gitea serves attachments with inline rendering (HTML, SVG), the renamed file hosts stored XSS against the Gitea origin
  • Operator hardening intent (the allowlist) is silently defeated, with no audit trail beyond the regular release-edit event

Suggested remediation

Mirror the parent CVE-2025-68939 fix into the web release edit path. In services/release/release.go UpdateRelease, verify each new name against the configured allowlist before persisting:

import (
    "code.gitea.io/gitea/modules/setting"
    "code.gitea.io/gitea/services/context/upload"
)

// inside UpdateRelease, replace the editAttachments loop:
for uuid, newName := range editAttachments {
    if deletedUUIDs.Contains(uuid) {
        continue
    }
    if err := upload.Verify(nil, newName, setting.Repository.Release.AllowedTypes); err != nil {
        return err
    }
    if err = repo_model.UpdateAttachmentByUUID(ctx, &repo_model.Attachment{
        UUID: uuid,
        Name: newName,
    }, "name"); err != nil {
        return err
    }
}

The web handler EditReleasePost should map IsErrFileTypeForbidden to a 422 response (or equivalent flash error and form re-render) to match the API behavior.

Alternative: refactor attachment_service.UpdateAttachment to accept a UUID (or expose a UpdateAttachmentByUUID variant in the service layer) and have the release service call that instead of the raw model function.

Workaround for operators (no Gitea change required)

Until a patched release lands, operators can mitigate by either:

  1. Removing the Repository.Release.AllowedTypes allowlist (accept any extension) -- this eliminates the bypass but also removes the defense, so it is only a holding move.
  2. Putting Gitea behind a reverse proxy that rewrites or strips suspicious attachment-edit-* form fields on POST to /<owner>/<repo>/releases/edit/* -- viable but operationally fragile.
  3. Restricting who has Write permission on repositories with a configured release allowlist -- in single-tenant deployments this may be acceptable.

A vendor patch is the right answer; the workarounds above are stopgaps.

Credit

Jose Rivas (bl4cksku111.com)

References

  • Parent advisory: https://github.com/advisories/GHSA-263q-5cv3-xq9g (CVE-2025-68939)
  • Parent fix: PR https://github.com/go-gitea/gitea/pull/32151 (commit 7adc4717ec)
  • CWE-424: https://cwe.mitre.org/data/definitions/424.html
  • CWE-434: https://cwe.mitre.org/data/definitions/434.html
Show details on source website

{
  "affected": [
    {
      "package": {
        "ecosystem": "Go",
        "name": "code.gitea.io/gitea"
      },
      "ranges": [
        {
          "events": [
            {
              "introduced": "0"
            },
            {
              "fixed": "1.27.0"
            }
          ],
          "type": "ECOSYSTEM"
        }
      ]
    }
  ],
  "aliases": [
    "CVE-2026-58428"
  ],
  "database_specific": {
    "cwe_ids": [
      "CWE-424",
      "CWE-434"
    ],
    "github_reviewed": true,
    "github_reviewed_at": "2026-07-21T20:18:44Z",
    "nvd_published_at": null,
    "severity": "MODERATE"
  },
  "details": "## Summary\n\nThe web handler `EditReleasePost` (`routers/web/repo/release.go`) reads form fields with prefix `attachment-edit-{uuid}` into a `map[uuid]newName`, passes that map to `release_service.UpdateRelease`, which writes the new name to the database via `repo_model.UpdateAttachmentByUUID` WITHOUT calling `upload.Verify` against `setting.Repository.Release.AllowedTypes`. The parent CVE-2025-68939 fix (PR #32151) added the equivalent `upload.Verify` call on the API edit endpoints via `attachment_service.UpdateAttachment`. The web release edit path was not updated.\n\nA user with repository write permission can rename any existing release attachment to a name with a forbidden extension via the web release edit form, bypassing the operator-configured allowlist.\n\n## Details\n\n### Vulnerable code\n\n`routers/web/repo/release.go:597` `EditReleasePost`:\n\n```go\nconst editPrefix = \"attachment-edit-\"\neditAttachments := make(map[string]string)\nif setting.Attachment.Enabled {\n    for k, v := range ctx.Req.Form {\n        if strings.HasPrefix(k, editPrefix) {\n            editAttachments[k[len(editPrefix):]] = v[0]\n        }\n    }\n}\n...\nif err = release_service.UpdateRelease(ctx, ctx.Doer, ctx.Repo.GitRepo,\n    rel, addAttachmentUUIDs, delAttachmentUUIDs, editAttachments); err != nil {\n    ctx.ServerError(\"UpdateRelease\", err)\n    return\n}\n```\n\n`services/release/release.go:321` -- the unvalidated write:\n\n```go\nfor uuid, newName := range editAttachments {\n    if !deletedUUIDs.Contains(uuid) {\n        if err = repo_model.UpdateAttachmentByUUID(ctx, \u0026repo_model.Attachment{\n            UUID: uuid,\n            Name: newName,\n        }, \"name\"); err != nil {\n            return err\n        }\n    }\n}\n```\n\nNo `upload.Verify(nil, newName, setting.Repository.Release.AllowedTypes)` before the database write.\n\n### Comparison: the parent fix on the API path\n\n`routers/api/v1/repo/release_attachment.go:341` (patched in PR #32151):\n\n```go\nif err := attachment_service.UpdateAttachment(ctx,\n        setting.Repository.Release.AllowedTypes, attach); err != nil {\n    if upload.IsErrFileTypeForbidden(err) {\n        ctx.Error(http.StatusUnprocessableEntity, \"\", err)\n        return\n    }\n    ctx.Error(http.StatusInternalServerError, \"UpdateAttachment\", attach)\n    return\n}\n```\n\nDelegates to:\n\n```go\n// services/attachment/attachment.go:96\nfunc UpdateAttachment(ctx context.Context, allowedTypes string, attach *repo_model.Attachment) error {\n    if err := upload.Verify(nil, attach.Name, allowedTypes); err != nil {\n        return err\n    }\n    return repo_model.UpdateAttachment(ctx, attach)\n}\n```\n\nThe API path goes through `attachment_service.UpdateAttachment` which calls `upload.Verify(nil, attach.Name, allowedTypes)`. The web path bypasses this entirely.\n\n## Proof of Concept\n\nTested live against:\n* Gitea `v1.26.1` community edition, Linux amd64, SQLite, Go 1.26.2\n* `app.ini` includes `[repository.release] ALLOWED_TYPES = .zip,.tar.gz`\n* Two users: `admin` (superuser, created via `gitea admin user create --admin`), `bob` (regular, repo owner of `bob/test-repo`)\n\n**Step 1**: bob creates release v0.1 and uploads `innocent.zip` (allowlist compliant) via the API.\n\n**Step 2**: Sanity. The patched API edit endpoint rejects a rename to a forbidden extension.\n\n```http\nPATCH /api/v1/repos/bob/test-repo/releases/1/assets/1 HTTP/1.1\nAuthorization: token \u003cbob_token\u003e\nContent-Type: application/json\n\n{\"name\":\"evil.exe\"}\n```\n\nResponse: `HTTP 422` -- \"This file cannot be uploaded or modified due to a forbidden file extension or type.\" (parent CVE-2025-68939 fix in action).\n\n**Step 3**: The attack. The web release edit form does NOT enforce the allowlist.\n\n```http\nPOST /bob/test-repo/releases/edit/v0.1 HTTP/1.1\nCookie: i_like_gitea=\u003csession\u003e; lang=en-US\nContent-Type: application/x-www-form-urlencoded\n\ntag_name=v0.1\n\u0026tag_target=main\n\u0026title=rename+payload\n\u0026content=\n\u0026attachment-edit-\u003cexisting_attachment_uuid\u003e=evil.exe\n```\n\nResponse: `HTTP 303 -\u003e /bob/test-repo/releases`. The form is accepted with no validation error.\n\n**Step 4**: Verify.\n\n```http\nGET /api/v1/repos/bob/test-repo/releases/1/assets/1 HTTP/1.1\n```\n\nResponse includes `\"name\": \"evil.exe\"`. The download link `/attachments/\u003cuuid\u003e` now serves the file under the forbidden extension.\n\nA self contained Python PoC ships with this advisory: `GITEA-R007_release_edit_extension_bypass.py`. End to end run:\n\n[GITEA-R007_release_edit_extension_bypass.py](https://github.com/user-attachments/files/27739265/GITEA-R007_release_edit_extension_bypass.py)\n\n```\n[+] Logged in as bob\n[+] Pre-attack attachment name: \u0027innocent2.zip\u0027\n[+] API endpoint correctly rejects rename: HTTP 422 (parent CVE-2025-68939 fix)\n[+] POST release edit: HTTP 303 -\u003e /bob/test-repo/releases\n[+] Post-attack attachment name: \u0027pwn.exe\u0027\n\n[!!!] CONFIRMED: web release edit bypasses Release.AllowedTypes allowlist.\n```\n\n## Impact\n\nSame impact class as the parent CVE-2025-68939 (HIGH, CVSS 8.2):\n\n* Pre-condition: operator has set `Repository.Release.AllowedTypes` to a non-empty allowlist (a reasonable hardening posture when restricting release uploads).\n* Threat actor: user holding repository write permission. In most Gitea deployments this is the repo owner, organization members, or invited collaborators.\n* Effect: bypass the allowlist; an attachment uploaded under an allowed extension is renamed to a forbidden extension (.exe, .html, .svg, .js, ...) and served by Gitea under that name.\n* Practical impact:\n  * Distribute malware files (e.g., `.exe`, `.dmg`, `.msi`, `.apk`) masquerading as a tagged release attachment\n  * If Gitea serves attachments with inline rendering (HTML, SVG), the renamed file hosts stored XSS against the Gitea origin\n  * Operator hardening intent (the allowlist) is silently defeated, with no audit trail beyond the regular release-edit event\n\n## Suggested remediation\n\nMirror the parent CVE-2025-68939 fix into the web release edit path. In `services/release/release.go UpdateRelease`, verify each new name against the configured allowlist before persisting:\n\n```go\nimport (\n    \"code.gitea.io/gitea/modules/setting\"\n    \"code.gitea.io/gitea/services/context/upload\"\n)\n\n// inside UpdateRelease, replace the editAttachments loop:\nfor uuid, newName := range editAttachments {\n    if deletedUUIDs.Contains(uuid) {\n        continue\n    }\n    if err := upload.Verify(nil, newName, setting.Repository.Release.AllowedTypes); err != nil {\n        return err\n    }\n    if err = repo_model.UpdateAttachmentByUUID(ctx, \u0026repo_model.Attachment{\n        UUID: uuid,\n        Name: newName,\n    }, \"name\"); err != nil {\n        return err\n    }\n}\n```\n\nThe web handler `EditReleasePost` should map `IsErrFileTypeForbidden` to a 422 response (or equivalent flash error and form re-render) to match the API behavior.\n\nAlternative: refactor `attachment_service.UpdateAttachment` to accept a UUID (or expose a `UpdateAttachmentByUUID` variant in the service layer) and have the release service call that instead of the raw model function.\n\n## Workaround for operators (no Gitea change required)\n\nUntil a patched release lands, operators can mitigate by either:\n\n1. Removing the `Repository.Release.AllowedTypes` allowlist (accept any extension) -- this eliminates the bypass but also removes the defense, so it is only a holding move.\n2. Putting Gitea behind a reverse proxy that rewrites or strips suspicious `attachment-edit-*` form fields on POST to `/\u003cowner\u003e/\u003crepo\u003e/releases/edit/*` -- viable but operationally fragile.\n3. Restricting who has Write permission on repositories with a configured release allowlist -- in single-tenant deployments this may be acceptable.\n\nA vendor patch is the right answer; the workarounds above are stopgaps.\n\n## Credit\n\nJose Rivas (bl4cksku111.com)\n\n## References\n\n* Parent advisory: https://github.com/advisories/GHSA-263q-5cv3-xq9g (CVE-2025-68939)\n* Parent fix: PR https://github.com/go-gitea/gitea/pull/32151 (commit `7adc4717ec`)\n* CWE-424: https://cwe.mitre.org/data/definitions/424.html\n* CWE-434: https://cwe.mitre.org/data/definitions/434.html",
  "id": "GHSA-25gq-j9jx-43pg",
  "modified": "2026-07-21T20:18:44Z",
  "published": "2026-07-21T20:18:44Z",
  "references": [
    {
      "type": "WEB",
      "url": "https://github.com/go-gitea/gitea/security/advisories/GHSA-25gq-j9jx-43pg"
    },
    {
      "type": "WEB",
      "url": "https://github.com/go-gitea/gitea/pull/38406"
    },
    {
      "type": "WEB",
      "url": "https://github.com/go-gitea/gitea/pull/38426"
    },
    {
      "type": "WEB",
      "url": "https://github.com/go-gitea/gitea/commit/de4b8277e9cb576f2315fb03b5ab6478b42a1d31"
    },
    {
      "type": "WEB",
      "url": "https://github.com/go-gitea/gitea/commit/f69e15afe7496cc62e96dab244629c69eb31a7bf"
    },
    {
      "type": "PACKAGE",
      "url": "https://github.com/go-gitea/gitea"
    },
    {
      "type": "WEB",
      "url": "https://github.com/go-gitea/gitea/releases/tag/v1.27.0"
    }
  ],
  "schema_version": "1.4.0",
  "severity": [
    {
      "score": "CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:N/I:H/A:N",
      "type": "CVSS_V3"
    }
  ],
  "summary": "Gitea: Release attachment extension allowlist bypass via web release edit form (variant of CVE-2025-68939)"
}



Log in or create an account to share your comment.




Tags
Taxonomy of the tags.


Loading…

Loading…

Loading…

Forecast uses a logistic model when the trend is rising, or an exponential decay model when the trend is falling. Fitted via linearized least squares.

Sightings

Author Source Type Date Other

Nomenclature

  • Seen: The vulnerability was mentioned, discussed, or observed by the user.
  • Confirmed: The vulnerability has been validated from an analyst's perspective.
  • Published Proof of Concept: A public proof of concept is available for this vulnerability.
  • Exploited: The vulnerability was observed as exploited by the user who reported the sighting.
  • Patched: The vulnerability was observed as successfully patched by the user who reported the sighting.
  • Not exploited: The vulnerability was not observed as exploited by the user who reported the sighting.
  • Not confirmed: The user expressed doubt about the validity of the vulnerability.
  • Not patched: The vulnerability was not observed as successfully patched by the user who reported the sighting.

Loading…

Detection rules are retrieved from Rulezet.

Loading…

Loading…