Return to Sender: Weaponizing SAML RelayState in Directus
Jan 6, 2026·6 min read
Executive Summary (TL;DR)
Directus failed to validate the `RelayState` parameter in its SAML driver during the authentication callback. While it checked redirect URLs at the start of a login attempt, it blindly trusted the state returned by the Identity Provider. This allows attackers to craft login links that, upon successful authentication, immediately bounce users to malicious domains—a perfect setup for high-credibility phishing campaigns.
A deep dive into an Open Redirect vulnerability within Directus's SAML authentication driver, allowing attackers to hijack post-login flows via unvalidated RelayState parameters.
The Hook: When State Becomes a Traitor
Directus is the darling of the headless CMS world—sleek, modular, and written in TypeScript. It powers everything from simple websites to complex enterprise data lakes. And when you step into the enterprise ring, you eventually have to wrestle with the beast known as SAML (Security Assertion Markup Language).
SAML is essentially XML-based suffering designed to allow Single Sign-On (SSO). One of its core features is the RelayState parameter. Think of RelayState as a coat check ticket. When a user leaves the Service Provider (Directus) to authenticate at the Identity Provider (IdP), they hand over this ticket. When they come back, authenticated and smiling, the IdP hands the ticket back so the Service Provider knows where they were trying to go (e.g., /admin/content/posts/123).
But here is the catch: if the Service Provider blindly trusts whatever is written on that returned ticket without checking if it actually belongs to the venue, you have a problem. That is exactly what happened here. Directus treated the RelayState as gospel, creating a perfect Open Redirect vector buried inside the trusted heavy machinery of enterprise authentication.
The Flaw: The 'Check at the Door' Fallacy
The vulnerability lies in a disconnect between the start and the finish of the authentication race. Directus actually has mechanisms to validate redirect URLs. When a user initiates a login, the system typically checks if the destination is in an allowlist. This is standard security hygiene.
However, in the specific implementation of the SAML driver (api/src/auth/drivers/saml.ts), this logic was applied inconsistently. The root cause analysis reveals a classic "Time-of-Check to Time-of-Use" style logic gap—not purely in race conditions, but in flow stages. The validation might occur when generating the initial SAML request, but the Assertion Consumer Service (ACS)—the endpoint that receives the user back from the IdP—did not re-validate the URL contained in the RelayState.
Because the IdP (like Okta, Auth0, or JumpCloud) is a trusted third party, developers often implicitly trust the data coming back from it. But RelayState is technically an opaque value often controlled by the initiator of the flow. If an attacker can start a flow (or manipulate the link starting the flow) with a malicious RelayState, the IdP dutifully echoes it back to Directus. Directus, seeing a valid login assertion, then executes the redirect. It’s like a bouncer checking your ID at the front door, but letting you walk out the back door with the silverware because "you're already inside."
The Code: Adding the Gatekeeper
Let's look at the "smoking gun" in api/src/auth/drivers/saml.ts. The vulnerability existed because the callback handler processed the login and immediately redirected the user if a relayState was present, assuming it was safe.
The fix, applied in commit dad9576ea9362905cc4de8028d3877caff36dc23, introduces a mandatory check using isLoginRedirectAllowed. This function presumably checks the target URL against the configured AUTH_ALLOW_LIST or ensures it is relative to the application root.
The Vulnerable Logic (Conceptual):
// Old behavior: Trust and Redirect
if (relayState) {
return redirect(relayState);
}The Patched Logic:
// api/src/auth/drivers/saml.ts
const authMode = (env[`AUTH_${providerName.toUpperCase()}_MODE`] ?? 'session') as string;
// The Fix: Validate the relayState before using it
if (relayState && isLoginRedirectAllowed(relayState, providerName) === false) {
throw new InvalidPayloadError({
reason: `URL "${relayState}" can't be used to redirect after login`
});
}
try {
const { sp, idp } = getAuthProvider(providerName) as SAMLAuthDriver;
// ... proceed with parsing login response ...This simple if statement closes the loop. It explicitly rejects any RelayState that doesn't pass the strict allowlist check, throwing an InvalidPayloadError instead of facilitating the attacker's redirect.
The Exploit: Hijacking the Handshake
Exploiting this requires understanding the SAML flow. An attacker does not need to compromise the IdP; they just need to generate a valid initiation link that the victim clicks.
Here is the attack chain:
- Craft the Bait: The attacker constructs a URL pointing to the Directus SAML login endpoint. In many SAML implementations, you can append a
RelayState(orredirect) parameter to the initiation URL. - The Hook: The URL looks like
https://directus.target-corp.com/auth/login/saml?redirect=https://evil-phishing-site.com. - The Legitimate Login: The victim clicks the link. They are taken to their real corporate Okta/SSO page. Everything looks 100% legitimate because it is legitimate. They enter their real credentials.
- The Bounce: The IdP authenticates the user and sends a POST request back to Directus with the SAML Assertion and the
RelayStatevalue (https://evil-phishing-site.com) preserved. - The Capture: Directus accepts the valid assertion, logs the user in, reads the
RelayState, and issues a302 Foundtohttps://evil-phishing-site.com.
The user believes they are still in the corporate flow. The attacker's site can now say "Session Expired, please re-enter password" to harvest credentials, or simply serve malware.
The Impact: Trust Erosion
Open Redirects are often dismissed as "Low Severity" by automated scanners and inexperienced managers. "So what? They go to Google.com?" is the common refrain.
This is dangerous thinking. In a post-login context, the impact is significantly higher. The user has just established trust by successfully authenticating. They are psychologically primed to accept the next screen as part of the application. If that next screen is a credential harvester hosted on support-directus-auth.com, the success rate of the phishing campaign skyrockets.
Furthermore, if the SSO token or session cookies are passed loosely (though less likely in modern SameSite configurations), there is potential for token leakage via the Referer header or URL parameters, potentially leading to immediate account takeover.
The Fix: Trust Nothing
The mitigation is straightforward: Upgrade. The Directus team patched this in early December 2025. The fix forces the RelayState through the same isLoginRedirectAllowed gauntlet that handles other redirects.
Immediate Action Plan:
- Upgrade: Update
@directus/apito the latest version (post-commitdad9576ea9362905cc4de8028d3877caff36dc23). - Audit Config: specific environment variables like
AUTH_ALLOW_LIST(or equivalent redirect allowlists) must be strictly defined. If you use wildcards (e.g.,*.mycompany.com), ensure you don't have dangling DNS entries that attackers could claim. - WAF Rules: As a temporary mitigation, you can inspect incoming traffic to the SAML ACS endpoint. If the POST body or query string contains a
RelayStatematching external domains not in your allowlist, block the request.
Official Patches
Fix Analysis (1)
Technical Appendix
CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:C/C:L/I:L/A:NAffected Systems
Affected Versions Detail
| Product | Affected Versions | Fixed Version |
|---|---|---|
@directus/api Directus | < Dec 9 2025 (Commit dad9576) | Post-Dec 9 2025 Release |
| Attribute | Detail |
|---|---|
| Vulnerability Type | Open Redirect |
| CWE ID | CWE-601 |
| Affected Component | api/src/auth/drivers/saml.ts |
| Attack Vector | Network (Web) |
| CVSS (Estimated) | 6.1 (Medium) |
| Patch Date | 2025-12-09 |
MITRE ATT&CK Mapping
The software redirects the user to a destination that is not part of the site's domain or an allowed list, which can assist in phishing attacks.
Known Exploits & Detection
Vulnerability Timeline
Subscribe to updates
Get the latest CVE analysis reports delivered to your inbox.