
PoC — origin validation error enabling Entra ID PRT SSO cookie exfiltration in linux-entra-sso (GHSA-g9vc-5j77-f2cm, CVE-2026-87005, CVSS 5.3).
CVE status: requested, pending assignment. This finding is published as GHSA-g9vc-5j77-f2cm. On CVE assignment this repository is renamed
CVE-YYYY-NNNNN-linux-entra-sso-PoCand this banner is replaced with the CVE link.
| Researcher | Dostxodjayev Abdullox (@squeeze440) |
| Advisory | GHSA-g9vc-5j77-f2cm |
| CVSS 3.1 | 5.3 (Medium) |
| Weakness | CWE-346, CWE-20 |
Platform.SSO_URL in linux-entra-sso lacks a trailing path separator, so the Firefox/Thunderbird onBeforeSendHeaders guard (e.url.startsWith(Platform.SSO_URL)) is bypassed by any attacker-registered domain of the form login.microsoftonline.com.<attacker-domain>, causing the extension to acquire and inject a live Entra ID Primary Refresh Token (PRT) SSO cookie into requests sent to the attacker's own host.
siemens/linux-entra-sso — Browser plugin for Linux to SSO on Microsoft Entra ID via the local Microsoft Identity Broker (Intune). Firefox build is practically exploitable; Thunderbird shares the identical vulnerable code path but is not currently reachable via this vector (see Details); Chrome/Chromium is not affected (see Details).
v1.10.0 (commit 676854c, current main as of 2026-08-03). Confirmed present in the current release, i.e. not fixed by GHSA-52rj-42vh-2rxc / CVE-2026-42177 (that fix touched only the Chrome declarativeNetRequest adapter).
5.3 (Medium) — CVSS:3.1/AV:N/AC:H/PR:N/UI:R/S:U/C:H/I:N/A:N
AC:H — exploitation requires the extension to hold a granted host permission that covers the attacker's crafted hostname. In practice the attacker must social-engineer the victim into clicking the extension's "Background SSO (enable)" link while on the attacker's page — a condition outside the attacker's direct control, matching the complexity rationale used for the sibling GHSA-52rj-42vh-2rxc.UI:R — the permission grant is an explicit user click.C:H — a stolen PRT SSO cookie is directly replayable against login.microsoftonline.com to mint access/ID tokens for any app the victim has consented to (full SSO session hijack).I:N/A:N — the bug discloses a credential but does not itself modify data or availability.Root cause — src/platform.js:9:
static SSO_URL = "https://login.microsoftonline.com";
No trailing /.
Vulnerable check — platform/firefox/js/platform-firefox.js:55-59:
async #onBeforeSendHeaders(e) {
// filter out requests that are not part of the OAuth2.0 flow
if (!e.url.startsWith(Platform.SSO_URL)) {
return { requestHeaders: e.requestHeaders };
}
...
String.prototype.startsWith performs a raw character-prefix comparison, not a hostname/origin comparison. Because Platform.SSO_URL has no trailing separator, any URL whose host is login.microsoftonline.com.<anything> — a syntactically valid DNS name the attacker fully controls — also satisfies the check, e.g.:
"https://login.microsoftonline.com.attacker.example/phish".startsWith("https://login.microsoftonline.com")
=> true
This is the same defensive check that GHSA-52rj-42vh-2rxc's own description called "the canonical defensive check ... absent from the Chrome adapter" — it exists here, but is itself bypassable, so the advisory's fix (which only hardened Chrome's declarativeNetRequest rule) left this path open. platform/thunderbird/js/platform-thunderbird.js extends PlatformFirefox without overriding update_request_handlers() or #onBeforeSendHeaders, so Thunderbird inherits the same flaw.
Trigger path (update_request_handlers, platform/firefox/js/platform-firefox.js:36-53): the listener is registered with urls: this.well_known_app_filters, which is populated from chrome.permissions.getAll().origins (src/platform.js:78-79, update_host_permissions()). Any host permission the user has granted — including a single-site grant via the popup's "Background SSO (enable)" link (popup/menu.js:171-260, using current_filter = "https://" + tab_hostname + "/*" derived from the active tab) — is enough to register the listener for that exact host. No catch-all https://*/* permission is required, unlike the exploitation prerequisite described in GHSA-52rj-42vh-2rxc; a single targeted grant on the attacker's own look-alike domain is sufficient here, which is a lower bar than the prior advisory's PoC.
Sink — same file, lines 60-70:
let prt = await this.#broker.acquirePrtSsoCookie(this.account, e.url);
ssoLog("inject PRT SSO into request headers");
e.requestHeaders.push({ name: prt.cookieName, value: prt.cookieContent });
e.url (the full attacker URL) is passed straight through as ssoUrl to the native broker. linux-entra-sso.py:27-30 documents that the Microsoft broker backend does not validate ssoUrl server-side ("the correct value is not checked ... by the authorization backend"), so a cookie is issued regardless, and it is injected into headers on the request actually going to the attacker's own host — a direct exfiltration, not merely a header set on a Microsoft-bound request.
Chrome is not affected: platform/chrome/js/platform-chrome.js:96-99 (post GHSA-52rj-42vh-2rxc fix, commit 8183759) anchors with "|" + Platform.SSO_URL + "/" (start-of-string anchor + trailing slash) and adds an explicit requestDomains: [URL.parse(Platform.SSO_URL).hostname] allow-list — both of which correctly reject a login.microsoftonline.com.attacker.example host.
Thunderbird — same vulnerable code, not currently reachable via this vector: platform/thunderbird/js/platform-thunderbird.js extends PlatformFirefox unchanged, so the identical startsWith bypass exists in the Thunderbird build's binary. However platform/thunderbird/manifest.json declares only a fixed "host_permissions": ["https://login.microsoftonline.com/*"] with no optional_host_permissions key. WebExtensions only allows chrome.permissions.request() to grant origins that are pre-declared in optional_permissions/optional_host_permissions; since Thunderbird declares none, the popup's "Background SSO (enable)" flow (popup/menu.js:259-261) cannot obtain permission for an attacker-chosen domain on Thunderbird today. The bug is latent there, not currently attacker-reachable, and should still be fixed for defense-in-depth / in case optional_host_permissions is ever added to that manifest.
Dynamically confirmed the vulnerable predicate against the real shipped constant (imported directly from the target's src/platform.js, not hand-copied). Script kept at ~/engagements/linux-entra-sso/evidence/prt_bypass_poc.mjs.
$ node prt_bypass_poc.mjs
Real Platform.SSO_URL from src/platform.js: "https://login.microsoftonline.com"
Legit MS URL passes filter (expected true): true
Attacker domain-suffix URL: https://login.microsoftonline.com.attacker.example/phish
Attacker URL passes filter (SHOULD be false, is): true
[CONFIRMED] Platform.SSO_URL lacks a trailing '/', so startsWith()
treats 'login.microsoftonline.com.attacker.example' as an in-scope
SSO endpoint. onBeforeSendHeaders would call:
broker.acquirePrtSsoCookie(this.account, "https://login.microsoftonline.com.attacker.example/phish")
and inject the returned PRT cookie into the request headers sent to
the attacker-controlled host.