PoC — error de validación de origen que permite la exfiltración de la cookie SSO PRT de Entra ID en linux-entra-sso (GHSA-g9vc-5j77-f2cm, CVE-2026-87005, CVSS 5.3).
Estado del CVE: solicitado, pendiente de asignación. Este hallazgo se publica como GHSA-g9vc-5j77-f2cm. Tras la asignación del CVE, este repositorio se renombra a
CVE-YYYY-NNNNN-linux-entra-sso-PoCy este banner se reemplaza por el enlace del CVE.
| Investigador | Dostxodjayev Abdullox (@squeeze440) |
| Aviso | GHSA-g9vc-5j77-f2cm |
| CVSS 3.1 | 5.3 (Medio) |
| Debilidad | CWE-346, CWE-20 |
Platform.SSO_URL en linux-entra-sso carece de un separador de ruta final, por lo que la protección onBeforeSendHeaders de Firefox/Thunderbird (e.url.startsWith(Platform.SSO_URL)) es eludida por cualquier dominio registrado por el atacante de la forma login.microsoftonline.com.<attacker-domain>, lo que provoca que la extensión adquiera e inyecte una cookie SSO de Primary Refresh Token (PRT) de Entra ID activa en las solicitudes enviadas al propio host del atacante.
siemens/linux-entra-sso — Complemento de navegador para Linux para SSO en Microsoft Entra ID a través del Microsoft Identity Broker local (Intune). La compilación para Firefox es prácticamente explotable; Thunderbird comparte la ruta de código vulnerable idéntica pero actualmente no es alcanzable mediante este vector (ver Detalles); Chrome/Chromium no se ve afectado (ver Detalles).
v1.10.0 (commit 676854c, main actual a fecha de 2026-08-03). Confirmado presente en la versión actual, es decir, no corregido por GHSA-52rj-42vh-2rxc / CVE-2026-42177 (esa corrección solo afectó al adaptador declarativeNetRequest de Chrome).
5.3 (Medio) — CVSS:3.1/AV:N/AC:H/PR:N/UI:R/S:U/C:H/I:N/A:N
AC:H — la explotación requiere que la extensión posea un permiso de host concedido que cubra el nombre de host manipulado por el atacante. En la práctica, el atacante debe engañar a la víctima mediante ingeniería social para que haga clic en el enlace "Background SSO (enable)" de la extensión mientras está en la página del atacante — una condición fuera del control directo del atacante, lo que coincide con la justificación de complejidad utilizada para el GHSA-52rj-42vh-2rxc hermano.UI:R — la concesión del permiso es un clic explícito del usuario.C:H — una cookie SSO PRT robada es directamente reutilizable contra login.microsoftonline.com para emitir tokens de acceso/ID para cualquier aplicación que la víctima haya consentido (secuestro completo de la sesión SSO).I:N/A:N — el fallo divulga una credencial pero no modifica por sí mismo datos ni disponibilidad.Causa raíz — src/platform.js:9:
static SSO_URL = "https://login.microsoftonline.com";
Sin / final.
Comprobación vulnerable — 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 realiza una comparación de prefijo de caracteres sin procesar, no una comparación de nombre de host/origen. Dado que Platform.SSO_URL no tiene un separador final, cualquier URL cuyo host sea login.microsoftonline.com.<anything> — un nombre DNS sintácticamente válido que el atacante controla por completo — también satisface la comprobación, por ejemplo:
"https://login.microsoftonline.com.attacker.example/phish".startsWith("https://login.microsoftonline.com")
=> true
Esta es la misma comprobación defensiva que la propia descripción de GHSA-52rj-42vh-2rxc denominó "la comprobación defensiva canónica ... ausente del adaptador de Chrome" — aquí existe, pero es a su vez eludible, por lo que la corrección del aviso (que solo reforzó la regla declarativeNetRequest de Chrome) dejó esta ruta abierta. platform/thunderbird/js/platform-thunderbird.js extiende PlatformFirefox sin sobrescribir update_request_handlers() ni #onBeforeSendHeaders, por lo que Thunderbird hereda el mismo defecto.
Ruta de activación (update_request_handlers, platform/firefox/js/platform-firefox.js:36-53): el listener se registra con urls: this.well_known_app_filters, que se rellena desde chrome.permissions.getAll().origins (src/platform.js:78-79, update_host_permissions()). Cualquier permiso de host que el usuario haya concedido — incluida una concesión de un solo sitio mediante el enlace "Background SSO (enable)" del popup (popup/menu.js:171-260, usando current_filter = "https://" + tab_hostname + "/*" derivado de la pestaña activa) — es suficiente para registrar el listener para ese host exacto. No se requiere un permiso comodín https://*/*, a diferencia del requisito de explotación descrito en GHSA-52rj-42vh-2rxc; aquí basta una única concesión dirigida sobre el propio dominio de aspecto similar del atacante, lo que supone un listón más bajo que el PoC del aviso anterior.
Sumidero — mismo archivo, líneas 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 (la URL completa del atacante) se pasa directamente como ssoUrl al broker nativo. linux-entra-sso.py:27-30 documenta que el backend del broker de Microsoft no valida ssoUrl en el lado del servidor ("the correct value is not checked ... by the authorization backend"), por lo que se emite una cookie independientemente, y se inyecta en las cabeceras de la solicitud que realmente va al propio host del atacante — una exfiltración directa, no meramente un conjunto de cabeceras en una solicitud dirigida a Microsoft.
Chrome no se ve afectado: platform/chrome/js/platform-chrome.js:96-99 (tras la corrección de GHSA-52rj-42vh-2rxc, commit 8183759) ancla con "|" + Platform.SSO_URL + "/" (ancla de inicio de cadena + barra final) y añade una lista de permitidos explícita requestDomains: [URL.parse(Platform.SSO_URL).hostname] — ambas rechazan correctamente un host login.microsoftonline.com.attacker.example.
Thunderbird — mismo código vulnerable, actualmente no alcanzable mediante este vector: platform/thunderbird/js/platform-thunderbird.js extiende PlatformFirefox sin cambios, por lo que el mismo bypass de startsWith existe en el binario de la compilación de Thunderbird. Sin embargo, platform/thunderbird/manifest.json declara únicamente un "host_permissions": ["https://login.microsoftonline.com/*"] fijo con ninguna clave optional_host_permissions. WebExtensions solo permite que chrome.permissions.request() conceda orígenes predeclarados en optional_permissions/optional_host_permissions; dado que Thunderbird no declara ninguno, el flujo "Background SSO (enable)" del popup (popup/menu.js:259-261) no puede obtener permiso para un dominio elegido por el atacante en Thunderbird actualmente. El fallo es latente allí, no alcanzable por el atacante en la actualidad, y aun así debería corregirse por defensa en profundidad / en caso de que optional_host_permissions se añada alguna vez a ese manifiesto.
Se confirmó dinámicamente el predicado vulnerable contra la constante real distribuida (importada directamente desde src/platform.js del objetivo, no copiada a mano). Script conservado en ~/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.
El resto de la cadena (el broker emitiendo una cookie para un ssoUrl no validado, y el listener webRequest de WebExtensions disparándose para un permiso de un solo host concedido) está trazado estáticamente, no confirmado dinámicamente — reproducirlo de extremo a extremo requiere un host Linux inscrito en vivo ejecutando microsoft-identity-broker contra un tenant real de Entra, lo cual queda fuera de este entorno. Los pasos trazados:
login.microsoftonline.com.attacker.example y sirve una página allí.popup/menu.js:259-261 → request_host_permission(["https://login.microsoftonline.com.attacker.example/*"])).chrome.permissions.onAdded → on_permissions_changed() (src/background.js:30-34) → update_host_permissions() actualiza well_known_app_filters → update_request_handlers() vuelve a registrar el listener webRequest.onBeforeSendHeaders de Firefox incluyendo el nuevo patrón de host.main_frame/sub_frame a https://login.microsoftonline.com.attacker.example/... dispara el listener; la comprobación startsWith (mostrada eludida arriba) pasa.broker.acquirePrtSsoCookie(account, e.url) devuelve una cookie PRT real (según linux-entra-sso.py:27-30, ssoUrl no es validado por el backend del broker).Un atacante que consigue que un usuario de linux-entra-sso en Firefox conceda "Background SSO" para un dominio de aspecto similar controlado por el atacante recibe la cookie SSO PRT de Entra ID de la víctima directamente en su propio servidor. Esa cookie es reutilizable contra login.microsoftonline.com para obtener tokens de acceso/ID para cualquier aplicación que la víctima haya consentido — secuestro completo de la sesión SSO. A diferencia del aviso anterior, esto no requiere el permiso comodín amplio https://*/*, solo una concesión de un solo sitio sobre el propio dominio del atacante. Thunderbird incluye la misma comprobación defectuosa pero su manifiesto actualmente no tiene forma de conceder en tiempo de ejecución un permiso de dominio arbitrario, por lo que no es explotable mediante este vector hoy (ver Detalles).
Anclar la comparación a una comprobación adecuada de origen/nombre de host en lugar de un prefijo de cadena sin procesar, replicando la corrección ya aplicada al adaptador de Chrome:
if (!e.url.startsWith(Platform.SSO_URL + "/")) {
return { requestHeaders: e.requestHeaders };
}
o, de forma más robusta, comparar new URL(e.url).origin con new URL(Platform.SSO_URL).origin (rechaza también trucos de userinfo/puerto). Aplicar la corrección a nivel de la constante Platform.SSO_URL (añadir la barra final una vez en src/platform.js) cerraría esto tanto para la comprobación de Firefox como para cualquier otro consumidor de la constante, y una prueba de regresión que verifique que startsWith rechaza https://login.microsoftonline.com.attacker.example/... cerraría la brecha de cobertura de la misma manera que la prueba de regresión testMatchOutcome sugerida en la corrección de Chrome lo hizo para GHSA-52rj-42vh-2rxc.
Dostxodjayev Abdullox (GitHub: squeeze440)
No hay ningún SECURITY.md presente en el repositorio y la API de GitHub no reporta ninguna política de seguridad a nivel de repositorio, pero GitHub Private Vulnerability Reporting está habilitado en siemens/linux-entra-sso (verificado mediante el botón "Report a vulnerability" de la pestaña Security del repositorio). Reportar a través de ese canal: https://github.com/siemens/linux-entra-sso/security/advisories/new.