All Posts programming Fixing a Sneaky Security Bug in Gitea: Users Could No Longer Change Someone Else’s Primary Email

Fixing a Sneaky Security Bug in Gitea: Users Could No Longer Change Someone Else’s Primary Email

· 680 words · 4 minute read
Go in production: interesting moments ▹

How two quick pull requests ( #36586 + #36607 ) closed an authorization hole in the account settings 🔗

TL;DR
A logged-in user could tamper with a form and change another user’s primary email address.
Fixed in 24 hours by adding a simple ownership check + better error message + tests.
Backported to Gitea 1.25 so everyone is protected.
Goal achieved ✅ No more ID tampering, clear error for users, and the code is now secure by design.

1. The Problem (Why This Bug Mattered) 🔗

Gitea’s User Settings → Account → Email addresses page lets you mark one of your emails as “Primary”.

Behind the scenes, the page sends a tiny hidden field containing the email record ID.
The server was supposed to say:

“Only the owner of this email can make it primary.”

But it didn’t check.

Result: Any logged-in user could change the primary email flag on any email record in the entire database — even emails belonging to other users.

This is a classic IDOR (Insecure Direct Object Reference) vulnerability. It didn’t let you steal accounts, but it could:

  • Confuse users (“Why did my primary email suddenly change?”)
  • Break notification flows or 2FA recovery
  • Open the door for more serious issues if combined with other bugs

2. How the Bug Actually Worked (Technical Breakdown) 🔗

When you clicked “Make Primary”, the browser sent something like:

POST /user/settings/account
_method=PRIMARY&id=42   ←←← anyone could put any number here

Old code in routers/web/user/setting/account.go (simplified):

if ctx.FormString("_method") == "PRIMARY" {
    err := user_model.MakeActiveEmailPrimary(ctx, ctx.FormInt64("id"))  // ← NO owner check!
    ...
}

The model function never verified that the email belonged to ctx.Doer (the logged-in user).
So Gitea happily updated the is_primary column on whoever’s email record had ID 42.

3. The Fix – What Changed (and Why It’s Elegant) 🔗

PR #36586 (main branch, merged Feb 12, 2026) by lunny did three smart things:

  1. Added an ownerID parameter to the core functions
    New helpers in models/user/email_address.go:

    func MakeActiveEmailPrimary(ctx context.Context, ownerID, emailID int64) error
    func MakeInactiveEmailPrimary(...)  // same pattern
    

    Inside makeEmailPrimaryInternal, Gitea now does:

    email, err := GetEmailAddressByID(ctx, emailID)
    if err != nil || email.UID != ownerID {
        return user_model.ErrEmailAddressNotExist{}  // reuse existing error
    }
    
  2. Updated the web handler to always pass the current user’s ID:

    if ctx.FormString("_method") == "PRIMARY" {
        if err := user_model.MakeActiveEmailPrimary(ctx, ctx.Doer.ID, ctx.FormInt64("id")); err != nil {
            if user_model.IsErrEmailAddressNotExist(err) {
                ctx.Flash.Error(ctx.Tr("settings.email_primary_not_found"))
                ...
            }
        }
    }
    
  3. Improved the user experience
    New translation string (English):

    “The selected email address could not be found.”

    Users now get a friendly flash message instead of a cryptic server error.

Bonus: Two new integration tests in tests/integration/user_settings_test.go that deliberately try invalid and foreign email IDs — ensuring the bug can never sneak back in.

Before vs After (visualized):

IDOR vulnerability in Gitea IDOR vulnerability in Gitea

Fixing IDOR vuln in Gitea Fixing IDOR vuln. in Gitea

4. The Backport – Everyone Gets the Fix 🔗

PR #36607 (released Feb 14, 2026) by the same author is a clean backport to the release/v1.25 branch.
It includes the exact same security fix plus tiny test + translation tweaks needed for the older release.
Now Gitea 1.25.5 users are protected too.

5. Goals of the Pull Requests – Fully Achieved 🔗

  • Security: No user can touch another user’s email records anymore.
  • Correctness: Ownership check is now explicit and centralized in the model layer.
  • Usability: Clear error message instead of silent failure or ugly stack trace.
  • Test coverage: New tests prevent regression.
  • Maintenance: Backported so the whole community benefits quickly.
  • Minimal change: The fix is small, readable, and follows Gitea’s existing patterns.

If you run a Gitea instance, update to the latest 1.26 or 1.25.5 patch.
The bug is gone, the code is cleaner, and your users’ email settings are finally safe from accidental (or intentional) tampering.

I hope you enjoyed reading this post as much as I enjoyed writing it. If you know a person who can benefit from this information, send them a link of this post. If you want to get notified about new posts, follow me on YouTube , Twitter (x) , LinkedIn , and GitHub .

Go in production: interesting moments ▹