Every CVE Search/ Get API key

CVE-2026-77561

Tinyauth: Unauthenticated login attempts can trigger global login lockdown denial of service

5.3 MediumFix available

Tinyauth is an authentication and authorization server. Prior to 5.1.0, an unauthenticated remote attacker can send POST /api/user/login requests with 257 distinct nonexistent usernames to fill MaxLoginAttemptRecords and activate a global login lockdown. internal/controller/user_controller.go loginHandler passes each attacker-controlled identifier to internal/service/auth_service.go RecordLoginAttempt, which invokes lockdownMode after the map reaches its cap. IsAccountLocked checks that global state before validating unrelated accounts, causing valid users to receive HTTP 429 until auth.loginTimeout expires, approximately 300 seconds by default. The attack can be repeated, but existing authenticated sessions are not invalidated. This issue is fixed in version 5.1.0.

Attack profile

How easy it is to exploit

Attack vector
Network
Complexity
Low
Privileges needed
None
User interaction
None

What an attacker can affect

Confidentiality
None
Integrity
None
Availability
Low

Exploit likelihood

0.60%Up 0.1 points since Sep 22, 2026
90%50%10%1%

What each source reports

CVE List Score used 5.3

Tinyauth: Unauthenticated login attempts can trigger global login lockdown denial of service

Tinyauth is an authentication and authorization server. Prior to 5.1.0, an unauthenticated remote attacker can send POST /api/user/login requests with 257 distinct nonexistent usernames to fill MaxLoginAttemptRecords and activate a global login lockdown. internal/controller/user_controller.go loginHandler passes each attacker-controlled identifier to internal/service/auth_service.go RecordLoginAttempt, which invokes lockdownMode after the map reaches its cap. IsAccountLocked checks that global state before validating unrelated accounts, causing valid users to receive HTTP 429 until auth.loginTimeout expires, approximately 300 seconds by default. The attack can be repeated, but existing authenticated sessions are not invalidated. This issue is fixed in version 5.1.0.

Record
CVE-2026-77561
CVSS 3.1
5.3 MediumCVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:L
Weakness
CWE-307
Updated
2026-11-02
EUVDEUVD-2026-84015 5.3

Tinyauth is an authentication and authorization server. Prior to 5.1.0, an unauthenticated remote attacker can send POST /api/user/login requests with 257 distinct nonexistent usernames to fill MaxLoginAttemptRecords and activate a global login lockdown. internal/controller/user_controller.go loginHandler passes each attacker-controlled identifier to internal/service/auth_service.go RecordLoginAttempt, which invokes lockdownMode after the map reaches its cap. IsAccountLocked checks that global state before validating unrelated accounts, causing valid users to receive HTTP 429 until auth.loginTimeout expires, approximately 300 seconds by default. The attack can be repeated, but existing authenticated sessions are not invalidated. This issue is fixed in version 5.1.0.

Record
EUVD-2026-84015
CVSS 3.1
5.3 MediumCVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:L
Updated
2026-11-02
GitHub AdvisoriesGHSA-9xhm-w3wj-xhqh 5.3

Tinyauth: Unauthenticated login attempts can trigger global login lockdown denial of service

Summary

Tinyauth's login rate-limit bookkeeping can enter a global lockdown mode when its in-memory login-attempt map reaches 256 distinct identifiers. Because unauthenticated POST /api/user/login requests for unknown usernames are recorded in this same map, a remote unauthenticated attacker can submit 257 unique bogus usernames and cause valid credentials for unrelated users to be treated as locked until auth.loginTimeout expires.

This was confirmed against the stable v5.0.7 release. With default configuration, auth.loginTimeout is 300 seconds and auth.loginMaxRetries is 3, so the denial lasts about 5 minutes and can be repeated.

Details

In stable v5.0.7, the login endpoint is registered at internal/controller/user_controller.go:45 and accepts unauthenticated JSON credentials in loginHandler at internal/controller/user_controller.go:50. Before validating credentials, it calls controller.auth.IsAccountLocked(req.Username) at internal/controller/user_controller.go:65.

When a username does not exist, the login handler records a failed login attempt for the attacker-controlled username with controller.auth.RecordLoginAttempt(req.Username, false) at internal/controller/user_controller.go:83. Invalid passwords for existing users do the same at internal/controller/user_controller.go:94.

The rate-limit map has a hard cap of 256 records at internal/service/auth_service.go:29. RecordLoginAttempt checks len(auth.loginAttempts) >= MaxLoginAttemptRecords at internal/service/auth_service.go:261 and, once the cap is reached, launches auth.lockdownMode() at internal/service/auth_service.go:265 instead of evicting old identifiers or rejecting only the new identifier.

lockdownMode sets a global auth.lockdown value with Active: true and ActiveUntil: now + auth.config.LoginTimeout at internal/service/auth_service.go:790-804. IsAccountLocked checks this global lockdown before looking up the requested identifier at internal/service/auth_service.go:227-234, so every username is treated as locked while the global lockdown is active.

The default configuration enables this path with LoginTimeout: 300 and LoginMaxRetries: 3 at internal/config/config.go:23-24.

Source-to-sink path:

Unauthenticated POST /api/user/login JSON username
  -> loginHandler binds LoginRequest
  -> unknown username path records RecordLoginAttempt(attacker-chosen username, false)
  -> 257 unique identifiers fill loginAttempts beyond MaxLoginAttemptRecords
  -> RecordLoginAttempt starts lockdownMode()
  -> lockdownMode sets global auth.lockdown.Active = true
  -> IsAccountLocked returns locked for unrelated valid users
  -> login endpoint returns HTTP 429 for valid credentials until loginTimeout expires

Candidate score: 13/14. Reachability 2, attacker control 2, privilege required 2, sink impact 1, mitigation weakness 2, default exposure 2, safe reproduction feasibility 2.

PoC

This PoC is local and non-destructive. It was tested against stable tag v5.0.7. It proves that 257 distinct unknown-user identifiers trigger global lockdown for an unrelated valid user, while that user's password still verifies successfully.

  1. Check out stable v5.0.7.

  2. Create internal/service/login_lockdown_stable_poc_test.go:

package service

import (
    "fmt"
    "testing"
    "time"

    "github.com/steveiliop56/tinyauth/internal/config"
    "github.com/steveiliop56/tinyauth/internal/utils/tlog"
    "github.com/stretchr/testify/require"
)

func TestPoCStableUnknownUsersTriggerGlobalLoginLockdown(t *testing.T) {
    tlog.NewTestLogger().Init()

    authServiceCfg := AuthServiceConfig{
        Users: []config.User{{
            Username: "testuser",
            Password: "$2a$10$ZwVYQH07JX2zq7Fjkt3gU.BjwvvwPeli4OqOno04RQIv0P7usBrXa", // password
        }},
        LoginTimeout:    2,
        LoginMaxRetries: 3,
    }

    authService := NewAuthService(authServiceCfg, &DockerService{},…
Record
GHSA-9xhm-w3wj-xhqh
CVSS 3.1
5.3 MediumCVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:L
Weakness
CWE-307
Updated
2026-11-02
NVD 5.3

Tinyauth is an authentication and authorization server. Prior to 5.1.0, an unauthenticated remote attacker can send POST /api/user/login requests with 257 distinct nonexistent usernames to fill MaxLoginAttemptRecords and activate a global login lockdown. internal/controller/user_controller.go loginHandler passes each attacker-controlled identifier to internal/service/auth_service.go RecordLoginAttempt, which invokes lockdownMode after the map reaches its cap. IsAccountLocked checks that global state before validating unrelated accounts, causing valid users to receive HTTP 429 until auth.loginTimeout expires, approximately 300 seconds by default. The attack can be repeated, but existing authenticated sessions are not invalidated. This issue is fixed in version 5.1.0.

Record
CVE-2026-77561
CVSS 3.1
5.3 MediumCVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:L
Weakness
CWE-307
Updated
2026-11-02
OSV 5.3

Tinyauth: Unauthenticated login attempts can trigger global login lockdown denial of service

Tinyauth is an authentication and authorization server. Prior to 5.1.0, an unauthenticated remote attacker can send POST /api/user/login requests with 257 distinct nonexistent usernames to fill MaxLoginAttemptRecords and activate a global login lockdown. internal/controller/user_controller.go loginHandler passes each attacker-controlled identifier to internal/service/auth_service.go RecordLoginAttempt, which invokes lockdownMode after the map reaches its cap. IsAccountLocked checks that global state before validating unrelated accounts, causing valid users to receive HTTP 429 until auth.loginTimeout expires, approximately 300 seconds by default. The attack can be repeated, but existing authenticated sessions are not invalidated. This issue is fixed in version 5.1.0.

Record
CVE-2026-77561
CVSS 3.1
5.3 MediumCVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:L
Weakness
CWE-307
Updated
2026-11-02
OSVGHSA-9xhm-w3wj-xhqh 5.3

Tinyauth: Unauthenticated login attempts can trigger global login lockdown denial of service

Summary

Tinyauth's login rate-limit bookkeeping can enter a global lockdown mode when its in-memory login-attempt map reaches 256 distinct identifiers. Because unauthenticated POST /api/user/login requests for unknown usernames are recorded in this same map, a remote unauthenticated attacker can submit 257 unique bogus usernames and cause valid credentials for unrelated users to be treated as locked until auth.loginTimeout expires.

This was confirmed against the stable v5.0.7 release. With default configuration, auth.loginTimeout is 300 seconds and auth.loginMaxRetries is 3, so the denial lasts about 5 minutes and can be repeated.

Details

In stable v5.0.7, the login endpoint is registered at internal/controller/user_controller.go:45 and accepts unauthenticated JSON credentials in loginHandler at internal/controller/user_controller.go:50. Before validating credentials, it calls controller.auth.IsAccountLocked(req.Username) at internal/controller/user_controller.go:65.

When a username does not exist, the login handler records a failed login attempt for the attacker-controlled username with controller.auth.RecordLoginAttempt(req.Username, false) at internal/controller/user_controller.go:83. Invalid passwords for existing users do the same at internal/controller/user_controller.go:94.

The rate-limit map has a hard cap of 256 records at internal/service/auth_service.go:29. RecordLoginAttempt checks len(auth.loginAttempts) >= MaxLoginAttemptRecords at internal/service/auth_service.go:261 and, once the cap is reached, launches auth.lockdownMode() at internal/service/auth_service.go:265 instead of evicting old identifiers or rejecting only the new identifier.

lockdownMode sets a global auth.lockdown value with Active: true and ActiveUntil: now + auth.config.LoginTimeout at internal/service/auth_service.go:790-804. IsAccountLocked checks this global lockdown before looking up the requested identifier at internal/service/auth_service.go:227-234, so every username is treated as locked while the global lockdown is active.

The default configuration enables this path with LoginTimeout: 300 and LoginMaxRetries: 3 at internal/config/config.go:23-24.

Source-to-sink path:

Unauthenticated POST /api/user/login JSON username
  -> loginHandler binds LoginRequest
  -> unknown username path records RecordLoginAttempt(attacker-chosen username, false)
  -> 257 unique identifiers fill loginAttempts beyond MaxLoginAttemptRecords
  -> RecordLoginAttempt starts lockdownMode()
  -> lockdownMode sets global auth.lockdown.Active = true
  -> IsAccountLocked returns locked for unrelated valid users
  -> login endpoint returns HTTP 429 for valid credentials until loginTimeout expires

Candidate score: 13/14. Reachability 2, attacker control 2, privilege required 2, sink impact 1, mitigation weakness 2, default exposure 2, safe reproduction feasibility 2.

PoC

This PoC is local and non-destructive. It was tested against stable tag v5.0.7. It proves that 257 distinct unknown-user identifiers trigger global lockdown for an unrelated valid user, while that user's password still verifies successfully.

  1. Check out stable v5.0.7.

  2. Create internal/service/login_lockdown_stable_poc_test.go:

package service

import (
    "fmt"
    "testing"
    "time"

    "github.com/steveiliop56/tinyauth/internal/config"
    "github.com/steveiliop56/tinyauth/internal/utils/tlog"
    "github.com/stretchr/testify/require"
)

func TestPoCStableUnknownUsersTriggerGlobalLoginLockdown(t *testing.T) {
    tlog.NewTestLogger().Init()

    authServiceCfg := AuthServiceConfig{
        Users: []config.User{{
            Username: "testuser",
            Password: "$2a$10$ZwVYQH07JX2zq7Fjkt3gU.BjwvvwPeli4OqOno04RQIv0P7usBrXa", // password
        }},
        LoginTimeout:    2,
        LoginMaxRetries: 3,
    }

    authService := NewAuthService(authServiceCfg, &DockerService{},…
Record
GHSA-9xhm-w3wj-xhqh
CVSS 3.1
5.3 MediumCVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:L
Weakness
CWE-307
Updated
2026-11-02
OSVGO-2026-6564 No score

Tinyauth: Unauthenticated login attempts can trigger global login lockdown denial of service in github.com/tinyauthapp/tinyauth

Tinyauth: Unauthenticated login attempts can trigger global login lockdown denial of service in github.com/tinyauthapp/tinyauth

Record
GO-2026-6564
Updated
2026-11-02

Affected products

Affected packages

Affected packages
PackageAffectedFixed
github.com/steveiliop56/tinyauth
Go
before 1.0.1-0.20260715123057-dade1e2c8f27
1.0.1-0.20260715123057-dade1e2c8f27
github.com/tinyauthapp/tinyauth
Go
before 1.0.1-0.20260715123057-dade1e2c8f27
1.0.1-0.20260715123057-dade1e2c8f27

History

History
RecordedChangedSources
first indexedCVE List, EUVD, GitHub Advisories, NVD, OSV

Dates are when Every CVE recorded each change. Upstream dates may differ.

In short

CVE-2026-77561 is a medium vulnerability (CVSS 5.3) in Steveiliop56 Tinyauth, published September 21, 2026. No source lists public exploit code. EPSS puts the chance of exploitation in the next 30 days at 0.60%, the 48th percentile. Fixed in github.com/steveiliop56/tinyauth 1.0.1-0.20260715123057-dade1e2c8f27.

References

Show 4 moreShow less
This page doesn't list everything

Left out here: the rest of 2 source descriptions. The JSON record has them.

MoveEnter Open