[Security] OIDC Post-Authentication Open Redirect
- Dominant language
- TypeScript
- Stars
- 22.8k
- Forks
- 783
- Avg merge
- 1d 11h
- Merged PRs (30d)
- 47
Description
### Describe the Bug
(reported via email on 1 June 2026 - no response)
I am reporting an open redirect vulnerability in the OIDC authentication flow of Pangolin. I tested against fosrl/pangolin:latest (v1.18.4, commit ebe1c7a) and confirmed the issue end-to-end.
The endpoint POST /api/v1/auth/idp/:idpId/oidc/generate-url is accessible without authentication. It accepts a "redirectUrl" field in the request body and stores it verbatim inside a server-signed JWT written to the p_oidc_state cookie. The server code itself contains a developer comment at line 175 of server/routers/idp/generateOidcUrl.ts that reads "// TODO: validate that this is safe", indicating awareness of the gap.
After the user successfully authenticates with the OIDC provider and the callback is processed, the server returns the arbitrary redirectUrl to the browser. The client component ValidateOidcToken.tsx (line 144) then navigates to that URL without checking whether it belongs to the configured dashboard domain:
if (redirectUrl.startsWith("http")) {
window.location.href = data.redirectUrl;
}
The frontend's cleanRedirect() function, which does restrict redirects to relative paths, is only applied before calling generateOidcUrlProxy in IdpLoginButtons.tsx and is therefore bypassed when an attacker calls the API directly.
### Environment
-
### To Reproduce
PoC:
Step 1 -- Set a poisoned OIDC state (no authentication required):
POST /api/v1/auth/idp/1/oidc/generate-url
X-CSRF-Token: x-csrf-protection
Content-Type: application/json
{"redirectUrl":"https://attacker.example.com/collect"}
Response HTTP 201 includes a Set-Cookie header for p_oidc_state. The JWT payload contains:
{"redirectUrl":"https://attacker.example.com/collect","state":"...","codeVerifier":"..."}
Step 2 -- Deliver the OIDC authorization URL from the response to the victim.
Step 3 -- The victim authenticates with the OIDC provider. They are redirected to /auth/idp/1/oidc/callback.
Step 4 -- The server validates the authorization code and session, then returns:
{"data": {"redirectUrl": "https://attacker.example.com/collect"}}
Step 5 -- The victim's browser navigates to [attacker.example.com](http://attacker.example.com/). The attacker can harvest credentials, session cookies, or display a phishing page.
Recommended fix:
Before signing the stateJwt in generateOidcUrl.ts, validate that redirectUrl is a relative path or belongs to the configured dashboard domain. The cleanRedirect() function already implements this logic and can be reused server-side. On the client side, ValidateOidcToken.tsx should also verify the returned redirectUrl matches the expected origin before navigating.
### Expected Behavior
-
Contributor guide
Assessment
This issue has not been assessed yet.