GHSA-X77V-Q46J-393G

Vulnerability from github – Published: 2026-07-21 20:22 – Updated: 2026-07-21 20:22
VLAI
Summary
Gitea: Blind SSRF in OAuth2 avatar synchronization via unvalidated OIDC picture claim
Details

Summary

When [oauth2_client] UPDATE_AVATAR = true is enabled, Gitea fetches the avatar URL received from an OAuth2/OIDC provider using Go's default HTTP client. The URL comes from the user's OAuth/OIDC avatar value, commonly the OIDC picture claim.

The affected code path calls http.Get(url) without applying outbound host or IP restrictions. A low-privileged user who can influence their own picture claim under an already-configured OAuth2/OIDC source can cause the Gitea server to make arbitrary outbound HTTP GET requests. This includes requests to loopback addresses, RFC 1918 private network addresses, and IPv4 link-local addresses such as 169.254.169.254.

This is a blind SSRF by default. Impact can increase in deployments where the Gitea host can reach cloud metadata services, localhost-only services, or internal services that return valid image data.

Details

The vulnerable sink is in routers/web/auth/oauth.go:

func oauth2UpdateAvatarIfNeed(ctx *context.Context, url string, u *user_model.User) {
    if setting.OAuth2Client.UpdateAvatar && len(url) > 0 {
        resp, err := http.Get(url)
        if err == nil {
            defer func() { _ = resp.Body.Close() }()
        }
        if err == nil && resp.StatusCode == http.StatusOK {
            data, err := io.ReadAll(io.LimitReader(resp.Body, setting.Avatar.MaxFileSize+1))
            if err == nil && int64(len(data)) <= setting.Avatar.MaxFileSize {
                _ = user_service.UploadAvatar(ctx, u, data)
            }
        }
    }
}

The caller is in routers/web/auth/oauth_signin_sync.go:

func oauth2SignInSync(ctx *context.Context, authSourceID int64, u *user_model.User, gothUser goth.User) {
    oauth2UpdateAvatarIfNeed(ctx, gothUser.AvatarURL, u)
    ...
}

gothUser.AvatarURL is derived from the OAuth2/OIDC provider's avatar value. For OIDC providers, this is commonly populated from the picture claim returned by the provider's userinfo endpoint or ID token.

The issue is that this value can be attacker-influenced in some common IdP configurations, while Gitea fetches it server-side using http.Get with no host/IP validation and no restricted transport.

Comparable outbound fetch paths in Gitea use hostmatcher.NewDialContext to enforce restrictions at TCP dial time. For example, repository migration uses an HTTP transport with host matching. The OAuth2 avatar synchronization path does not apply those restrictions.

PoC

Requirements

  • Local Gitea build or binary
  • Python 3
  • Python packages: requests, pyjwt, cryptography
  • Gitea configured with OAuth2 avatar synchronization enabled

Install Python dependencies:

python3 -m pip install requests pyjwt cryptography

Configure app.ini:

[oauth2_client]
UPDATE_AVATAR = true
ENABLE_AUTO_REGISTRATION = true
USERNAME = userid

Run the fake OIDC provider:

python3 fake_oidc.py http://127.0.0.1:8888/ 9999

Run a listener for the SSRF target:

nc -lvnp 8888

Register an OAuth2 authentication source in Gitea:

  • Provider: OpenID Connect
  • Client ID: gitea-client
  • Client Secret: gitea-secret
  • OpenID Connect Auto Discovery URL: http://127.0.0.1:9999/.well-known/openid-configuration

Then initiate login through the configured OAuth2 source.

Observed request to the SSRF listener:

GET / HTTP/1.1
Host: 127.0.0.1:8888
User-Agent: Go-http-client/1.1
Accept-Encoding: gzip

Observe that Gitea fetched the OIDC picture claim URL from the server side using the default Go HTTP client.

Impact

When [oauth2_client] UPDATE_AVATAR = true is enabled, a low-privileged OAuth2/OIDC user who can influence their own picture claim can force the Gitea server to make outbound HTTP GET requests to attacker-selected URLs. This allows blind SSRF from the Gitea server’s network position, including requests to loopback addresses, RFC1918 private addresses, link-local addresses such as 169.254.169.254, and other internal services that may not be reachable from the public internet. In practical terms, this can enable internal service probing and interaction with localhost-only or private-network services depending on the deployment’s network access controls.

The vulnerability is blind in the common case because non-image responses such as HTML, JSON, or plaintext are rejected during avatar processing and are not directly returned to the attacker. However, impact can increase in cloud or internal-network deployments where metadata services, internal admin panels, monitoring endpoints, or image-generating internal services are reachable from the Gitea host. If an internal endpoint returns a valid supported image format within the configured avatar size limit, the response may be stored as the attacker’s avatar, creating a limited response retrieval primitive.

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-23603"
  ],
  "database_specific": {
    "cwe_ids": [
      "CWE-918"
    ],
    "github_reviewed": true,
    "github_reviewed_at": "2026-07-21T20:22:23Z",
    "nvd_published_at": null,
    "severity": "LOW"
  },
  "details": "### Summary\nWhen `[oauth2_client] UPDATE_AVATAR = true` is enabled, Gitea fetches the avatar URL received from an OAuth2/OIDC provider using Go\u0027s default HTTP client. The URL comes from the user\u0027s OAuth/OIDC avatar value, commonly the OIDC `picture` claim.\n\nThe affected code path calls `http.Get(url)` without applying outbound host or IP restrictions. A low-privileged user who can influence their own `picture` claim under an already-configured OAuth2/OIDC source can cause the Gitea server to make arbitrary outbound HTTP GET requests. This includes requests to loopback addresses, RFC 1918 private network addresses, and IPv4 link-local addresses such as `169.254.169.254`.\n\nThis is a blind SSRF by default. Impact can increase in deployments where the Gitea host can reach cloud metadata services, localhost-only services, or internal services that return valid image data.\n\n### Details\nThe vulnerable sink is in `routers/web/auth/oauth.go`:\n\n```go\nfunc oauth2UpdateAvatarIfNeed(ctx *context.Context, url string, u *user_model.User) {\n    if setting.OAuth2Client.UpdateAvatar \u0026\u0026 len(url) \u003e 0 {\n        resp, err := http.Get(url)\n        if err == nil {\n            defer func() { _ = resp.Body.Close() }()\n        }\n        if err == nil \u0026\u0026 resp.StatusCode == http.StatusOK {\n            data, err := io.ReadAll(io.LimitReader(resp.Body, setting.Avatar.MaxFileSize+1))\n            if err == nil \u0026\u0026 int64(len(data)) \u003c= setting.Avatar.MaxFileSize {\n                _ = user_service.UploadAvatar(ctx, u, data)\n            }\n        }\n    }\n}\n```\n\nThe caller is in `routers/web/auth/oauth_signin_sync.go`:\n\n```go\nfunc oauth2SignInSync(ctx *context.Context, authSourceID int64, u *user_model.User, gothUser goth.User) {\n    oauth2UpdateAvatarIfNeed(ctx, gothUser.AvatarURL, u)\n    ...\n}\n```\n\n`gothUser.AvatarURL` is derived from the OAuth2/OIDC provider\u0027s avatar value. For OIDC providers, this is commonly populated from the `picture` claim returned by the provider\u0027s userinfo endpoint or ID token.\n\nThe issue is that this value can be attacker-influenced in some common IdP configurations, while Gitea fetches it server-side using `http.Get` with no host/IP validation and no restricted transport.\n\nComparable outbound fetch paths in Gitea use `hostmatcher.NewDialContext` to enforce restrictions at TCP dial time. For example, repository migration uses an HTTP transport with host matching. The OAuth2 avatar synchronization path does not apply those restrictions.\n\n### PoC\n\n#### Requirements\n\n- Local Gitea build or binary\n- Python 3\n- Python packages: `requests`, `pyjwt`, `cryptography`\n- Gitea configured with OAuth2 avatar synchronization enabled\n\nInstall Python dependencies:\n\n```bash\npython3 -m pip install requests pyjwt cryptography\n```\n\nConfigure `app.ini`:\n\n```ini\n[oauth2_client]\nUPDATE_AVATAR = true\nENABLE_AUTO_REGISTRATION = true\nUSERNAME = userid\n```\n\nRun the fake OIDC provider:\n\n```bash\npython3 fake_oidc.py http://127.0.0.1:8888/ 9999\n```\n\nRun a listener for the SSRF target:\n\n```bash\nnc -lvnp 8888\n```\n\nRegister an OAuth2 authentication source in Gitea:\n\n- Provider: `OpenID Connect`\n- Client ID: `gitea-client`\n- Client Secret: `gitea-secret`\n- OpenID Connect Auto Discovery URL: `http://127.0.0.1:9999/.well-known/openid-configuration`\n\nThen initiate login through the configured OAuth2 source.\n\nObserved request to the SSRF listener:\n\n```http\nGET / HTTP/1.1\nHost: 127.0.0.1:8888\nUser-Agent: Go-http-client/1.1\nAccept-Encoding: gzip\n```\n\nObserve that Gitea fetched the OIDC `picture` claim URL from the server side using the default Go HTTP client.\n\n### Impact\nWhen [oauth2_client] UPDATE_AVATAR = true is enabled, a low-privileged OAuth2/OIDC user who can influence their own picture claim can force the Gitea server to make outbound HTTP GET requests to attacker-selected URLs. This allows blind SSRF from the Gitea server\u2019s network position, including requests to loopback addresses, RFC1918 private addresses, link-local addresses such as 169.254.169.254, and other internal services that may not be reachable from the public internet. In practical terms, this can enable internal service probing and interaction with localhost-only or private-network services depending on the deployment\u2019s network access controls.\n\nThe vulnerability is blind in the common case because non-image responses such as HTML, JSON, or plaintext are rejected during avatar processing and are not directly returned to the attacker. However, impact can increase in cloud or internal-network deployments where metadata services, internal admin panels, monitoring endpoints, or image-generating internal services are reachable from the Gitea host. If an internal endpoint returns a valid supported image format within the configured avatar size limit, the response may be stored as the attacker\u2019s avatar, creating a limited response retrieval primitive.",
  "id": "GHSA-x77v-q46j-393g",
  "modified": "2026-07-21T20:22:23Z",
  "published": "2026-07-21T20:22:23Z",
  "references": [
    {
      "type": "WEB",
      "url": "https://github.com/go-gitea/gitea/security/advisories/GHSA-x77v-q46j-393g"
    },
    {
      "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:H/PR:L/UI:N/S:U/C:L/I:N/A:N",
      "type": "CVSS_V3"
    }
  ],
  "summary": "Gitea: Blind SSRF in OAuth2 avatar synchronization via unvalidated OIDC picture claim"
}



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…

Loading…