CVE-2026-86117 bypasses both the password and two-factor authentication on a self-hosted platform-as-a-service, was first reported 84 days before it published, and names no fixed version.
What happened
NVD published CVE-2026-86117 at 2026-09-05T10:16:42.860, assigned by [email protected]. The record reads, in full:
“Coolify through 4.3.17 contains an authentication bypass vulnerability in the OAuth callback handler that signs users into existing accounts based solely on email address without verifying provider assertions or binding OAuth identities. Attackers can register a victim’s email address on any enabled OAuth provider to obtain authenticated sessions as that user, bypassing password requirements and two-factor authentication.”
Coolify is a self-hosted deployment platform, run in place of a managed PaaS. An authenticated session on one can deploy containers, read environment variables and secrets, reach connected servers, and take database backups. It is infrastructure control, not application access.
The phrase to weigh in that record is “through 4.3.17.” Coolify publishes its own release manifest at cdn.coollabs.io/coolify/versions.json, and at the time of writing that file reads {"v4": {"version": "4.3.17"}, "nightly": {"version": "4.4-rc.1"}, ...}. So 4.3.17 is not a historical ceiling. It is the current stable release. The affected range runs up to and includes the version a Coolify operator is running today if they are up to date.
VulnCheck’s advisory names no fixed version. The coollabsio/coolify repository’s security advisories page lists ten published GHSAs — seven critical, two high, one low, the critical batch all dated January 3, 2026 — and none of them corresponds to this flaw. There is, as of this writing, no vendor advisory and no patched build.
The code
The CVE record links the file directly: app/Http/Controllers/OauthController.php at tag v4.3.17. The callback() method obtains the profile from the provider, extracts the email, trims and lowercases it, aborts if it is empty, and then does this:
$user = User::whereEmail($email)->first();
followed, after an optional auto-registration branch gated on the instance’s registration setting, by:
Auth::login($user);
There is no check of whether the provider marked the email verified, no OAuth identity table binding a provider account to a local user, and no two-factor step in the OAuth path. The email string is the entire credential. The same code is present on the repository’s main branch.
Coolify’s own documentation states the design intent plainly: “Using OAuth, you can delegate authorization to get a user’s email address to an external IDP provider.” That is an accurate description of what the code does. It is also the problem — getting an email address from a provider and treating that email address as proof of identity are two different things, and the handler does the first while behaving as though it did the second.
Timeline and scope
The researcher, George Chen, published a writeup alongside the record. It gives an initial report by email on June 13, 2026 and a follow-up on July 19, 2026, with disclosure on September 5, 2026 — 84 days from first contact. The proof of concept is a PHPUnit feature test that authenticates as a victim account holding a password, a verified email, and confirmed two-factor authentication, by invoking the OAuth callback with a mocked attacker profile carrying the victim’s email address. The attack path is the ordinary one: the provider redirect at /auth/<provider>/redirect, then /auth/<provider>/callback.
Two scope facts matter and cut in opposite directions. OAuth is not on by default: Coolify’s documentation requires an administrator to obtain a client ID and secret and enter them at /settings/oauth, and to register a redirect URI, before any provider is enabled. Azure, Bitbucket, GitHub, GitLab, and Google are the supported providers. An instance with no OAuth provider configured is not exposed by this. But the record says “any enabled OAuth provider,” and an instance is only as strong as the weakest provider its administrator switched on — including a self-hosted GitLab, where the attacker may control the instance that asserts the email.
The record also carries two scores from the same CNA, in two different severity bands. VulnCheck’s CVSS v3.1 vector is CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:H/A:H, base 8.1, HIGH. Its CVSS v4.0 vector is CVSS:4.0/AV:N/AC:H/AT:P/PR:N/UI:N/VC:H/VI:H/VA:H/SC:N/SI:N/SA:N, base 9.2, CRITICAL. Same flaw, same assigner, same day, one HIGH and one CRITICAL. Any organization whose patching policy keys off “critical” will or will not act on this depending on which metric its scanner happens to ingest.
Why it matters
The interesting part is not that a login handler trusted an unverified email. That is a known and old mistake. It is where in the stack it happened, and what it costs to fix.
Email-as-identity is defensible in one situation: when the party asserting the email verified it, and the relying party checks that it did. Providers publish that information — Google returns email_verified, GitHub distinguishes verified from unverified addresses. A handler that reads the claim can refuse an unverified one. A handler that reads only the address cannot tell an assertion apart from a string. Once several providers are enabled, the account is protected by the least careful of them.
That is the second-order problem, and it outlasts any patch. Because there is no OAuth identity table, there is no record of which provider identity was used to enter an account. After a fix ships, an operator wanting to know whether this was ever exploited against them has no artifact to query — the successful login looks exactly like a legitimate one. Retrofitting binding is also the expensive kind of fix: existing users have to be linked to provider identities after the fact, and the linking step is itself an authentication decision. This is why “just check email_verified” is a smaller change than what the record actually asks for, which is “binding OAuth identities.”
The two-factor detail is the part worth carrying to a security committee. Organizations increasingly treat enforced two-factor authentication as the compensating control that lets them accept residual risk elsewhere. Here the second factor is not defeated; it is never consulted, because the OAuth path is a parallel entrance that was built without it. That is a recurring shape in self-hosted software: the password login gets hardened over releases, and the alternate login path inherits none of it. An inventory of “which of our systems enforce MFA” that is built from the password flow will be wrong for every system with a second door.
And the 84-day interval is worth naming plainly rather than editorializing. A report on June 13, a follow-up on July 19, disclosure on September 5, no vendor advisory, no fixed version, and the affected ceiling equal to the current release. Coolify’s project has demonstrated it can publish advisories — ten of them, seven critical, on a single day in January. The machinery exists. It has not been used here yet.
What to do
- Check whether OAuth is configured at all. Visit
/settings/oauthon your instance. If no provider has a client ID and secret entered, this does not apply to you, and the highest-value action is to leave it that way until a fix lands. - If a provider is enabled, disable it. There is no patched version to move to, so removing the callback path is the mitigation. Users fall back to password plus their existing second factor, which the OAuth path was bypassing anyway.
- If you cannot disable OAuth, cut it to one provider you control end to end — a corporate Azure or Google tenant where you own the email namespace and no outsider can create an account bearing one of your users’ addresses. Self-hosted GitLab as an enabled provider is the worst case here, because the assertion comes from a machine you may not own.
- Turn off open registration in instance settings while you are in there. The auto-registration branch in the same handler creates accounts from provider-supplied email and name.
- Review access rather than logs. There is no provider binding to audit against, so the check that works is the inventory one: list the accounts on the instance, confirm each is a person who should have infrastructure control, and rotate the secrets and keys any unexpected account could have read.
Sourcing note
The CVE record was read from NVD’s API; it is vulnStatus: Received, so no NVD enrichment or CPE configuration exists yet and the CNA text is the whole record. The current release version is from Coolify’s own manifest at cdn.coollabs.io/coolify/versions.json, read directly, not from a third party. The vulnerable code was read from raw.githubusercontent.com at tag v4.3.17 and again on main. The absence of a vendor advisory is based on the repository’s published security advisories listing, which was retrieved once.
The disclosure timeline and the proof of concept come from the researcher’s own writeup, linked from the CVE record. GitHub’s REST API returned HTTP 403 to automated retrieval from here, and the repository’s releases page returned tags dated 2024 that are inconsistent with the 4.3.17 in Coolify’s manifest — so no claim is made here about any individual release note. The version manifest is the only release-state source relied on.
One sourcing caveat on the documentation quote. Two retrievals of the same Coolify OAuth documentation page returned differently worded sentences describing OAuth’s purpose. Only the sentence returned by the retrieval that was asked for verbatim text is quoted above. A reader checking the page may find the surrounding wording differs.
Unresolved: whether Coolify has responded to the researcher privately, and whether a fix is in the 4.4-rc.1 nightly. Neither could be established from here. Also unresolved is why the same CNA assigned scores in two different severity bands on the same day; the vectors above are reproduced exactly so a reader can judge the AC:H and AT:P calls for themselves.
