GHSA-pxcx-fv34-x9p5: Race Condition

Published Sep 21, 2026
·
Updated

Summary

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.

Affected component

- Endpoint: POST /api/users/onboarding/finish - Route registration: api/user/routes.go:49 → authorizer.AllowAnonymous(http.MethodPost, "/api/users/onboarding/finish") - Handler: api/user/onboardingfinishhandler.go

Technical details

The route is explicitly allowed without authentication:

go // 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:

go // api/user/onboardingfinishhandler.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.

Preconditions (stated honestly)

This is exploitable when the instance is in a pre-onboarding state:

1. Fresh deployment — the time window between the service coming online and the legitimate operator completing onboarding. During this window any unauthenticated party who can reach the instance can register the first/an additional admin. The race lets an attacker slip an admin account in alongside the operator's, so the operator's onboarding appears to succeed normally while the attacker silently holds admin. 2. State reset — if onboarding state can return to "not completed" (e.g. all users removed), the endpoint reopens and becomes a repeatable unauthenticated admin-creation primitive.

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.

Proof of concept

Against an instance that has not yet completed onboarding:

bash 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 "%{httpcode}\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.

Impact

- Unauthenticated administrative account takeover of a fresh (or reset) instance. - Full admin enables every downstream capability: creating hosts/routes, editing global and per-route nginx configuration, managing access lists and certificates, and controlling the nginx server process. (The config surface is itself injectable — see the related nginx-configuration-injection issues — so admin here is a path to SSRF / arbitrary nginx directives.) - The TOCTOU race additionally allows minting multiple admin accounts from a single concurrent burst, aiding persistence/stealth.

Remediation

1. Make onboarding completion atomic: enforce a database-level unique constraint (e.g. "at most one onboarding user" / single-row guard) so concurrent creates collide, or wrap the check-and-create in a single transaction / mutex. 2. Re-check OnboardingCompleted() inside the same transaction that performs the insert, and abort on conflict. 3. Consider requiring a one-time setup token (printed to server logs / env at first boot) for the initial admin creation, eliminating the unauthenticated window entirely.

--- Finding ID: GM-4607

Affected Software

1 affected componentFixes available
go/github.com/lucasdillmann/nginx-ignition<0.0.0-20260621194639-0586b4e55ab
0.0.0-20260621194639-0586b4e55ab

Remediation

Recommended actions to resolve this vulnerability, in priority order.

  1. Upgrade

    Upgrade go/github.com/lucasdillmann/nginx-ignition to a version that resolves this vulnerability.

    Fixed in 0.0.0-20260621194639-0586b4e55ab
  2. Configuration

    Change routing/authorization so POST /api/users/onboarding/finish is not registered as anonymous (authenticated-only or otherwise gated) instead of authorizer.AllowAnonymous(http.MethodPost, "/api/users/onboarding/finish").

    Onboarding route authorization (api/user/routes.go) authorizer.AllowAnonymous(http.MethodPost, "/api/users/onboarding/finish") = Remove/disable anonymous allowance
  3. Configuration

    Make onboarding completion atomic by protecting the check-then-act TOCTOU between OnboardingCompleted() and Save() with a single transaction/lock or a database-level unique constraint so concurrent calls cannot both create admin users (abort on conflict).

    Onboarding completion enforcement (OnboardingCompleted check + Save) Atomicity/guard for onboarding completion and admin creation = Enforce atomic guard or DB constraint
  4. Compensating control

    Require a one-time setup token for the initial admin creation (printed to server logs/env at first boot) so there is no unauthenticated setup window during fresh deployment/state reset.

Event History

Sep 21, 2026
Advisory Published
via GitHub·09:54 PM
Data Sourced
via GitHub·09:54 PM
DescriptionSeverityWeaknessAffected Software

Frequently Asked Questions

1

Which deployments are exposed to unauthenticated administrator creation?

Instances are exposed while they remain in the pre-onboarding state and the POST /api/users/onboarding/finish endpoint is reachable by a remote attacker. The route is explicitly registered to allow anonymous requests.

2

What does an attacker need to exploit this issue?

An attacker does not need an existing account or user interaction. They need network access to an instance before onboarding has completed, and exploitation relies on racing the onboarding-state check against user creation.

3

Can this result in more than one unauthorized administrator account?

Yes. Concurrent requests can win the check-then-act race and create multiple accounts, each with full ReadWrite administrative permissions.

4

How can I tell whether an instance may already have been exploited?

Review user accounts for unexpected accounts with full ReadWrite permissions, particularly on instances that were reachable before onboarding was completed. The provided data does not identify specific account names or logging indicators.

Contact

SecAlerts Pty Ltd.
132 Wickham Terrace
Fortitude Valley,
QLD 4006, Australia
info@secalerts.co
By using SecAlerts services, you agree to our services end-user license agreement. This website is safeguarded by reCAPTCHA and governed by the Google Privacy Policy and Terms of Service. All names, logos, and brands of products are owned by their respective owners, and any usage of these names, logos, and brands for identification purposes only does not imply endorsement. If you possess any content that requires removal, please get in touch with us.
© 2026 SecAlerts Pty Ltd.
ABN: 70 645 966 203, ACN: 645 966 203