Gitea CVE-2026-20896: un header HTTP e il tuo server Git è compromesso
CVE-2026-20896 permette a un attaccante non autenticato di falsificare un singolo header HTTP e impersonare qualsiasi admin Gitea — leggendo repository privati, secret e deploy key. Ecco come rimediare.
Un server Git self-hosted è l'unica macchina che custodisce le chiavi di tutte le altre. Contiene il codice sorgente, le pipeline CI/CD, i deploy token e — che piaccia o no — le API key e le password di database che qualcuno ha committato per sbaglio tre anni fa. Quindi quando un authentication bypass critico colpisce la forge self-hosted più diffusa e comincia a essere sondato in the wild nel giro di due settimane, "lo patchiamo il prossimo sprint" non è un piano. CVE-2026-20896 è quel bug: CVSS 9.8, nessuna password, nessun token, un solo header HTTP.
Come funziona l'auth bypass di Gitea CVE-2026-20896
Gitea supporta l'autenticazione tramite reverse proxy: metti un proxy SSO davanti a Gitea, il proxy autentica l'utente e inoltra l'identità a Gitea in un header HTTP — X-WEBAUTH-USER. Gitea si fida di quell'header perché si fida del proxy. La sicurezza dell'intero schema poggia su un'unica assunzione: solo il tuo proxy può raggiungere Gitea, e Gitea accetta l'header soltanto dall'IP del proxy.
Quell'assunzione è governata da due chiavi di configurazione — ENABLE_REVERSE_PROXY_AUTHENTICATION e REVERSE_PROXY_TRUSTED_PROXIES. La seconda è l'allowlist degli IP da cui Gitea accetta l'header di identità. E nell'immagine Docker ufficiale fino alla 1.26.2 inclusa, quell'allowlist arrivava così:
REVERSE_PROXY_TRUSTED_PROXIES = *
Un wildcard. Ogni IP sorgente del pianeta è un "trusted proxy." Quindi su qualsiasi Gitea dockerizzato con reverse-proxy auth abilitata, un attaccante non autenticato invia:
GET /user/settings HTTP/1.1
Host: git.vittima.example
X-WEBAUTH-USER: admin
Gitea confronta l'IP sorgente con l'allowlist (* matcha tutto), vede un proxy "fidato" che asserisce che l'utente è admin, e autentica la richiesta come admin. Nessuna credenziale viene verificata perché, in questo modello, la credenziale è l'header. Il team di threat research di Sysdig l'ha detto senza giri di parole:
Un header HTTP. Accesso su qualsiasi Gitea esposto in internet. Nessuna password. Nessun token.
Il CVE è classificato come CWE-284, improper access control, ed è arrivato dentro una security release Gitea 1.26.3 che correggeva nove CVE — inclusa una SSRF separata da CVSS 9.6 nei webhook (CVE-2026-22874) che un attaccante appena diventato admin è nella posizione perfetta per concatenare.
Perché una forge è il posto peggiore per un auth bypass
Accesso in lettura come admin su un server di source control non significa "leggere un po' di codice." È la cima di un imbuto supply-chain. Una volta accettato l'header, l'attaccante eredita tutto ciò che l'account impersonato può vedere:
- Repository privati — sorgenti proprietari, e ogni secret mai committato al loro interno. Basta un grep nella history:
AWS_SECRET,.env,id_rsa,password=. Gli sviluppatori lo fanno per sbaglio di continuo; il repository ricorda per sempre. - Configurazione CI/CD e secret delle Actions — le definizioni delle pipeline più le variabili cifrate (credenziali di registry, chiavi cloud, materiale di firma) che vengono iniettate a build time.
- Deploy key e access token — le chiavi SSH e i PAT che permettono alla forge di fare push in produzione, tirare dipendenze private o autenticarsi al registry.
- Accesso in scrittura — impersonare l'admin significa poter anche fare push. Avvelenare uno script di build, aggiungere uno step CI malevolo o piazzare una backdoor in una dipendenza che i tuoi altri progetti consumano.
È quest'ultimo punto la ragione per cui un bug su una forge è categoricamente peggiore di un bug su una wiki interna. La copertura di SecurityWeek sull'exploitation attiva descrive esattamente questa progressione: impersonare un utente, leggere i repository privati, raccogliere le API key, le credenziali di database e i deploy token committati per errore, poi pivotare all'esterno usando le credenziali trovate. Il server Git è l'accesso iniziale e il magazzino delle credenziali in un solo salto.
Dalla disclosure all'exploitation in 13 giorni
Gitea ha rimosso il wildcard nella security release e reso l'autenticazione reverse-proxy opt-in. Poi la realtà ha fatto ciò che fa sempre. I sensori di Sysdig hanno intercettato la prima exploitation in the wild circa 13 giorni dopo la disclosure — uno scanner con uscita VPN che spruzzava l'header X-WEBAUTH-USER contro istanze Gitea esposte in internet, raccogliendo quelle che rispondevano. Shodan mostrava circa 6.200 server Gitea esposti in internet; quanti girino sul default Docker vulnerabile è ignoto, ed è precisamente il problema — nessuno può dirlo dall'esterno, e nemmeno gli operatori senza controllare la propria configurazione.
Tredici giorni non sono un margine comodo. Sono la finestra tra "la fix esiste" e "qualcuno ha automatizzato l'exploit," e si è chiusa mentre la maggior parte dei team stava ancora decidendo se un tool di sviluppo self-hosted contasse come produzione.
Perché l'auth bypass di Gitea sopravvive alla patch
Ecco la trappola, ed è la ragione per cui una dashboard di version-scanning che diventa verde vale meno di quanto sembri.
Primo, la falla è uno stato di configurazione, non solo un percorso di codice. La fix 1.26.3/1.26.4 cambia il default — il wildcard sparisce e la reverse-proxy auth diventa opt-in. Ma un operatore che ha impostato esplicitamente REVERSE_PROXY_TRUSTED_PROXIES = * nella propria config (copiato da un vecchio tutorial, o trascinato avanti attraverso gli upgrade) resta completamente esposto dopo la patch, perché il pacchetto rispetta il valore che hai messo tu. Due server sull'identica build corretta possono avere esposizione opposta. Un banner scan legge la versione e li dichiara entrambi sicuri. Solo testare davvero se l'istanza in esecuzione accetta un header falsificato dice la verità.
Secondo, ciò che è già stato rubato sopravvive all'upgrade. La patch chiude la porta; non fa "de-leakare" il deploy token che un attaccante ha copiato la settimana scorsa, non sfratta la chiave SSH che ha aggiunto a un repository, non fa rollback dello step CI malevolo che ha pushato. I secret che erano leggibili restano validi finché non li ruoti tu. È la stessa lezione che l'industria continua a re-imparare dai bug di edge che leakano token: la fix del CVE e l'incident response sono due lavori distinti, e saltare il secondo lascia intatto l'accesso dell'attaccante dietro un'etichetta "patchato."
E attenzione al rischio collaterale: Forgejo è un hard fork indipendente di Gitea, quindi l'advisory Gitea non si mappa automaticamente sui numeri di versione Forgejo — ma lo stesso pericoloso pattern wildcard su REVERSE_PROXY_TRUSTED_PROXIES vale la pena auditarlo anche lì.
Remediation
Tratta qualsiasi Gitea raggiungibile da internet con reverse-proxy auth come compromesso-finché-non-provato-pulito. Lavora la lista dall'alto in basso.
1. Sono interessato? Controlla la versione e — soprattutto — la config:
# Versione (Docker)
docker exec <gitea> gitea --version
# Le due chiavi che contano — in app.ini o come variabili GITEA__security__*
grep -Ei 'REVERSE_PROXY_TRUSTED_PROXIES|ENABLE_REVERSE_PROXY_AUTHENTICATION' \
/data/gitea/conf/app.ini
Sei esposto se ENABLE_REVERSE_PROXY_AUTHENTICATION = true e REVERSE_PROXY_TRUSTED_PROXIES è * (o non impostato su una build Docker vulnerabile) e Gitea è raggiungibile da qualcosa che non sia il proxy. Se la reverse-proxy auth è disabilitata, questo bug specifico non si applica — ma patcha comunque per gli altri otto CVE della release.
2. Patch — versione corretta esatta. Aggiorna alla 1.26.4 o successiva. Non fermarti alla 1.26.3: ha corretto il CVE ma introdotto una regressione sulle pagine del codice dei repository, risolta nella 1.26.4 (vedi le release notes di Gitea).
3. Non puoi patchare subito? — controlli compensativi.
- Imposta l'allowlist sul tuo proxy reale, non sul mondo:
[security] REVERSE_PROXY_LIMIT = 1 REVERSE_PROXY_TRUSTED_PROXIES = 127.0.0.1,172.18.0.1 # IP del bridge Docker / proxy - Se non usi affatto l'header auth SSO, imposta
ENABLE_REVERSE_PROXY_AUTHENTICATION = false. - Assicurati che la porta HTTP di Gitea sia raggiungibile solo dal reverse proxy — mai bindarla su
0.0.0.0su un'interfaccia pubblica. L'intero modello di fiducia assume che il proxy sia l'unico client.
4. Cerca segni di compromissione (mappati su MITRE ATT&CK):
- T1190 / T1606 (Exploit Public-Facing App / Forge Web Credentials): cerca nei log di accesso del reverse proxy e di Gitea gli header
X-WEBAUTH-USERprovenienti da un IP sorgente che non sia il tuo proxy. Qualsiasi richiesta simile da internet è exploitation. - T1078 (Valid Accounts): rivedi i login admin e privilegiati senza corrispondente stabilimento di sessione/SSO. Controlla in Site Administration → Users account o privilegi admin che non hai creato.
- T1213.003 / T1552 (Code Repositories / Credentials in Files): cerca picchi anomali di clone/pull e audita ogni repository che gli account esposti potevano leggere alla ricerca di secret committati.
- T1195 / persistenza: controlla chiavi SSH, deploy key, access token, applicazioni OAuth2 e modifiche ai webhook aggiunte di recente (queste ultime concatenano anche la SSRF CVE-2026-22874). Rivedi i push recenti e le modifiche ai workflow CI/Actions in cerca di step iniettati.
5. Bonifica + verifica. Dopo aver patchato e corretto la config: elimina account, chiavi, token e webhook non autorizzati; poi ruota tutto ciò che il server poteva leggere — secret committati, variabili CI/CD, deploy token, deploy key, credenziali di registry e cloud, PAT utente. Ricostruisci e ri-scansiona le pipeline eseguite durante la finestra di esposizione in cerca di step iniettati. Conferma la pulizia dopo la patch, non prima — una scansione pre-patch non certifica nulla.
Dove la validazione continua cambia l'esito
Tutto quanto sopra dipende da un fatto scomodo: dalla stringa di versione non puoi sapere se il tuo Gitea accetta un header X-WEBAUTH-USER falsificato. Dipende dalla tua config, dalla tua collocazione di rete e dal fatto che un vecchio wildcard sia ancora annidato in app.ini. È un problema di validazione, ed è esattamente il gap che un generative pentest è costruito per chiudere.
Lo swarm di 10 agenti AI di Zero Hunt non legge il banner e tira a indovinare. Gli agenti Web e Credential scrivono una probe per-target contro la tua istanza in esecuzione — inviano l'header craftato, da una posizione sorgente non fidata, e osservano se il server autentica la richiesta. Ogni exploit è generato localmente per l'ambiente che ha davanti, non pescato da un database di PoC, e ogni tentativo è backtestato nell'AI Gym contro corpora come Vulhub e NYU CTF Bench prima di girare in produzione, poi eseguito in un sandbox Docker effimero e irrobustito con gVisor perché il test non tocchi mai l'host. Quando l'header viene accettato, gli agenti Post-Exploit e Pivot fanno ciò che farebbe un vero intruso — enumerano i repository leggibili, portano in superficie i secret e le deploy key committate, e mappano il raggio d'impatto supply-chain — firmando ogni finding con ECDSA per una catena di custodia che prova che l'istanza era davvero sfruttabile, non solo "una versione vecchia." Una campagna change-triggered rigira nel momento in cui un nuovo Gitea compare sul perimetro, chiudendo quella finestra di 13 giorni prima che uno scanner lo trovi per primo.
E poiché l'exploitation stessa è un'iniezione di header seguita da esfiltrazione massiva di repo e secret, lascia anche una firma sul filo: il modello on-prem di AI Traffic Analysis di Zero Hunt — quattro teste di inferenza a 2.7+ Gbit/s sulla GPU dell'appliance — segnala il pattern di autenticazione anomalo e l'improvviso volume di clone in uscita da un host che normalmente serve solo traffico interno, mentre il furto sta avvenendo, non nella revisione dei log del mattino dopo. Patcha il default; poi dimostra che la porta è davvero chiusa, e tieni d'occhio il filo nel caso non lo fosse.