| Field | Value |
|---|---|
| Chain Name | User Enumeration + Brute Force = Account Takeover (3 vulnerabilities) |
| CWE IDs | CWE-203 (x2) + CWE-307 |
| CVSS 4.0 Score | 8.7 (High) |
| CVSS 4.0 Vector | CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:H/VI:H/VA:N/SC:N/SI:N/SA:N |
| Affected Software | Gitea |
| Affected Versions | <= 1.25.4 |
| Tested Version | 1.25.4 - 369830bada |
Gitea <= 1.25.4 is vulnerable to a user enumeration and brute force attack chain. Attackers can enumerate valid usernames through two methods: (1) accessing the /{username} user profile endpoint, where HTTP 200 indicates existence and 404 indicates non-existence; (2) submitting existing usernames to the /user/sign_up registration endpoint, which returns the error message "The username is already taken". Subsequently, attackers exploit the lack of rate limiting on the login endpoint to perform password brute force attacks at 280+ attempts/second, ultimately achieving arbitrary account takeover.
| Condition | Type | Required | Notes |
|---|---|---|---|
| User profile publicly accessible | Configuration | Partial | Default configuration allows anonymous access, enumeration method one depends on this |
| Registration enabled | Configuration | Partial | DISABLE_REGISTRATION=false (default), enumeration method two depends on this |
| Login CAPTCHA not enabled | Configuration | Yes | REQUIRE_CAPTCHA_FOR_LOGIN=false (default) |
| No external WAF/IDS protection | Environment | Partial | External protection may mitigate attacks |
| Target user uses weak password | User Action | Yes | Attack success depends on password strength |
The attacker uses a username dictionary, sequentially accessing the http://target/{username} endpoint. The system returns HTTP 200 status code and displays the user profile page for existing users, and returns HTTP 404 for non-existing users. By analyzing response status codes, the attacker can quickly enumerate all valid usernames in the system.
The attacker submits each username from the dictionary to the /user/sign_up registration endpoint. When a username already exists, the system returns the error message "The username is already taken"; when a username is available, the system attempts to create an account or returns other errors. By analyzing error message differences, the attacker can enumerate valid usernames. This method remains effective when user profile access is restricted.
Before starting brute force, the attacker sends 20 rapid login requests to test whether the system has rate limiting. If there is no HTTP 429 response or "too many requests" error message, it confirms the system has not implemented effective brute force protection.
For each enumerated user, the attacker sends login requests to the /user/login endpoint using a password dictionary. The system returns the same error message for each failed login attempt, does not record failure counts, does not lock accounts, allowing the attacker to continue attempts at 200+ per second.
When login succeeds (HTTP 302/303 redirect to non-login page), the attacker obtains a valid session. Subsequently accessing /user/settings confirms login status, accessing /admin checks if it's an administrator account, completing the account takeover.
Please check the POC_CODE_HERE
| Field | Value |
|---|---|
| Vulnerability ID | VULN-001 |
| Vulnerability Name | User Enumeration via Public Profile Access |
| Vulnerability Type | Information Disclosure |
| CWE-ID | CWE-203 (Observable Discrepancy) |
| CVSS 4.0 Score | 5.3 (Medium) |
| CVSS 4.0 Vector | CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:L/VI:N/VA:N/SC:N/SI:N/SA:N |
Gitea defaults to allowing anonymous access to user profile pages /{username}. When the requested username exists, the server returns HTTP 200 and displays the user profile page; when the username does not exist, it returns HTTP 404. Attackers can exploit this discrepancy to quickly enumerate valid usernames in the system, providing a target list for subsequent brute force or social engineering attacks.
File: routers/web/user/home.go:715-768
func UsernameSubRoute(ctx *context.Context) {
// WORKAROUND to support usernames with "." in it
// https://github.com/go-chi/chi/issues/781
username := ctx.PathParam("username")
reloadParam := func(suffix string) (success bool) {
ctx.SetPathParam("username", strings.TrimSuffix(username, suffix))
context.UserAssignmentWeb()(ctx)
if ctx.Written() {
return false
}
// check view permissions
if !user_model.IsUserVisibleToViewer(ctx, ctx.ContextUser, ctx.Doer) {
ctx.NotFound(fmt.Errorf("%s", ctx.ContextUser.Name))
return false
}
return true
}File: routers/web/web.go:820-823
m.Group("", func() {
m.Get("/{username}", user.UsernameSubRoute)
m.Methods("GET, OPTIONS", "/attachments/{uuid}", optionsCorsHandler(), repo.GetAttachment)
}, optSignIn)- SOURCE: HTTP GET request arrives at
/{username}route (routers/web/web.go:821) - Route matches to
user.UsernameSubRoutehandler function UsernameSubRouteextractsusernameparameter from URL path (routers/web/user/home.go:718)- Calls
context.UserAssignmentWeb()middleware (services/context/user.go:15) - Middleware calls
userAssignmentfunction (services/context/user.go:61) userAssignmentcallsuser_model.GetUserByName(ctx, username)to query database (services/context/user.go:68)- SINK: If user does not exist, returns HTTP 404; if exists, continues to render user profile returning HTTP 200
Gitea's user profile pages are open to unauthenticated users by default, and return different HTTP status codes (200 vs 404) for existing and non-existing users. The system lacks username enumeration protection mechanisms and has not implemented a unified error response strategy, allowing attackers to distinguish valid and invalid usernames through simple HTTP requests.
| Condition | Required | Notes |
|---|---|---|
| User profile publicly accessible | Yes | Default configuration |
| No WAF/IDS protection | Partial | External protection may detect enumeration behavior |
| Field | Value |
|---|---|
| Vulnerability ID | VULN-002 |
| Vulnerability Name | User Enumeration via Registration Error Messages |
| Vulnerability Type | Information Disclosure |
| CWE-ID | CWE-203 (Observable Discrepancy) |
| CVSS 4.0 Score | 5.3 (Medium) |
| CVSS 4.0 Vector | CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:L/VI:N/VA:N/SC:N/SI:N/SA:N |
Gitea's registration endpoint /user/sign_up returns the explicit error message "The username is already taken" when a username already exists, while returning other responses (registration success or other errors) when a username is available. Attackers can exploit this discrepancy to enumerate valid usernames in the system. This method remains effective when user profile access is restricted, serving as an alternative approach for user enumeration.
File: routers/web/auth/auth.go:579-586
// handle error with template
switch {
case user_model.IsErrUserAlreadyExist(err):
ctx.Data["Err_UserName"] = true
ctx.RenderWithErr(ctx.Tr("form.username_been_taken"), tpl, form)
case user_model.IsErrEmailAlreadyUsed(err):
ctx.Data["Err_Email"] = true
ctx.RenderWithErr(ctx.Tr("form.email_been_used"), tpl, form)
// ... other error cases ...
}File: routers/web/auth/auth.go:452-526
// SignUpPost response for sign up information submission
func SignUpPost(ctx *context.Context) {
form := web.GetForm(ctx).(*forms.RegisterForm)
// ...
u := &user_model.User{
Name: form.UserName,
Email: form.Email,
Passwd: form.Password,
}
if !createAndHandleCreatedUser(ctx, tplSignUp, form, u, nil, nil) {
// error already handled
return
}
}- SOURCE: HTTP POST request arrives at
/user/sign_uproute, carryinguser_nameparameter SignUpPostfunction handles the request (routers/web/auth/auth.go:453)- Creates
user_model.Userstruct containing user-submitted username - Calls
createAndHandleCreatedUserfunction (routers/web/auth/auth.go:523) - Calls
createUserInContextfunction (routers/web/auth/auth.go:535) - Internally calls
user_model.CreateUserto attempt user creation - If username already exists, returns
ErrUserAlreadyExisterror - SINK:
createUserInContextcatches error, renders "form.username_been_taken" message (routers/web/auth/auth.go:583)
Gitea's registration functionality returns explicit error messages when handling username conflicts, distinguishing between "username already exists" and other error types. The system has not implemented a unified registration error response strategy, allowing attackers to distinguish valid and invalid usernames by analyzing error messages. Additionally, registration functionality has no CAPTCHA protection by default, allowing automated batch enumeration.
| Condition | Required | Notes |
|---|---|---|
| Registration enabled | Yes | DISABLE_REGISTRATION=false (default) |
| Registration CAPTCHA not enabled | Partial | Can be bypassed via OCR or manual effort |
| No WAF/IDS protection | Partial | External protection may detect enumeration behavior |
| Field | Value |
|---|---|
| Vulnerability ID | VULN-003 |
| Vulnerability Name | Login Brute Force due to Missing Rate Limiting |
| Vulnerability Type | Authentication Bypass |
| CWE-ID | CWE-307 (Improper Restriction of Excessive Authentication Attempts) |
| CVSS 4.0 Score | 7.5 (High) |
| CVSS 4.0 Vector | CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:H/VI:N/VA:N/SC:N/SI:N/SA:N |
Gitea's login endpoint /user/login has CAPTCHA disabled by default, has not implemented login failure count limiting, has not implemented account lockout mechanism, and has not implemented IP-level rate limiting. Attackers can send hundreds of login requests per second, using password dictionaries to brute force target accounts until finding the correct password.
File: routers/web/auth/auth.go:196-243
// SignInPost response for sign in request
func SignInPost(ctx *context.Context) {
// ... validation code ...
if setting.Service.EnableCaptcha && setting.Service.RequireCaptchaForLogin {
context.VerifyCaptcha(ctx, tplSignIn, form)
if ctx.Written() {
return
}
}
u, source, err := auth_service.UserSignIn(ctx, form.UserName, form.Password)
if err != nil {
if errors.Is(err, util.ErrNotExist) || errors.Is(err, util.ErrInvalidArgument) {
ctx.RenderWithErr(ctx.Tr("form.username_password_incorrect"), tplSignIn, &form)
log.Warn("Failed authentication attempt for %s from %s: %v", form.UserName, ctx.RemoteAddr(), err)
}
// ... error handling continues without rate limiting ...
return
}File: modules/setting/service.go:56-57,190-191
// Service settings structure
type ServiceSettings struct {
// ...
EnableCaptcha bool
RequireCaptchaForLogin bool
// ...
}
// Default configuration loading
Service.EnableCaptcha = sec.Key("ENABLE_CAPTCHA").MustBool(false)
Service.RequireCaptchaForLogin = sec.Key("REQUIRE_CAPTCHA_FOR_LOGIN").MustBool(false)- SOURCE: HTTP POST request arrives at
/user/loginroute - Request carries
user_nameandpasswordparameters SignInPostfunction handles the request (routers/web/auth/auth.go:197)- Checks
RequireCaptchaForLoginconfiguration (default false, skips CAPTCHA) - Calls
auth_service.UserSignIn()to verify credentials (services/auth/signin.go:25) - SINK: If credentials are wrong, only logs and returns error message, no restriction measures
- Attacker can immediately send next request, no delay or blocking
Gitea's login functionality was not designed with brute force protection as a default security policy. The RequireCaptchaForLogin configuration defaults to false, and the system does not enforce CAPTCHA. The code does not implement login failure counters, temporary account lockout, IP blacklisting, or request delays and other protection mechanisms. Login failures are only written to logs, triggering no defensive actions.
| Condition | Required | Notes |
|---|---|---|
REQUIRE_CAPTCHA_FOR_LOGIN=false |
Yes | Default configuration |
ENABLE_CAPTCHA=false |
Partial | Even if CAPTCHA is enabled, login may still not enforce it |
| No external rate limiting | Yes | No reverse proxy or WAF limiting |
| Target uses weak password | Yes | Attack success depends on password strength |