CVE-2026-77561
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.
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
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 Medium
CVSS: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 Medium
CVSS: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.
-
Check out stable
v5.0.7. -
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 Medium
CVSS: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 Medium
CVSS: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 Medium
CVSS: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.
-
Check out stable
v5.0.7. -
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 Medium
CVSS: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
| Package | Affected | Fixed |
|---|---|---|
github.com/steveiliop56/tinyauthGo |
before 1.0.1-0.20260715123057-dade1e2c8f27 | 1.0.1-0.20260715123057-dade1e2c8f27 |
github.com/tinyauthapp/tinyauthGo |
before 1.0.1-0.20260715123057-dade1e2c8f27 | 1.0.1-0.20260715123057-dade1e2c8f27 |
History
| Recorded | Changed | Sources |
|---|---|---|
| first indexed | CVE 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
github.com/tinyauthapp/tinyauth/security/advisories/GHSA-9xhm-w3wj-xhqh
github.com/tinyauthapp/tinyauth/pull/1008
github.com/tinyauthapp/tinyauth/pull/943
github.com/tinyauthapp/tinyauth/commit/654b5cc436fc67865c1f55edf9ba9fbded50b74f
github.com/tinyauthapp/tinyauth/commit/dade1e2c8f27a23df56ac216dcaf4b37081698e7
github.com/tinyauthapp/tinyauth/releases/tag/v5.1.0
Left out here: the rest of 2 source descriptions. The JSON record has them.