diff --git a/.jules/sentinel.md b/.jules/sentinel.md new file mode 100644 index 0000000000..372e0aeb66 --- /dev/null +++ b/.jules/sentinel.md @@ -0,0 +1,4 @@ +## 2026-03-01 - [OAuth postMessage Origin Hardening] +**Vulnerability:** Insecure use of `postMessage` with wildcard origin (`'*'`) and weak origin validation using `startsWith`. +**Learning:** OAuth redirect handlers often use `window.opener.postMessage` to send authorization codes back to the main application window. Using `'*'` allows any site that can open or reference the redirect window to intercept these codes. Furthermore, using `startsWith` for origin validation is bypassable (e.g., `https://trusted.com.attacker.com` starts with `https://trusted.com`). +**Prevention:** Always use a specific target origin in `postMessage` calls. For origin validation in message listeners, use strict equality with a derived origin from a trusted URL (e.g., `new URL(trustedUrl).origin === event.origin`). Always add safety checks for URL parsing. diff --git a/packages/react-ui/src/app/routes/redirect.tsx b/packages/react-ui/src/app/routes/redirect.tsx index 0d7ccb0ed7..74db8bfcd4 100644 --- a/packages/react-ui/src/app/routes/redirect.tsx +++ b/packages/react-ui/src/app/routes/redirect.tsx @@ -13,7 +13,7 @@ const RedirectPage: React.FC = React.memo(() => { { code: code, }, - '*', + window.location.origin, ); } }, [location.search]); diff --git a/packages/react-ui/src/lib/oauth2-utils.ts b/packages/react-ui/src/lib/oauth2-utils.ts index 7b9ca004e9..8563a8173e 100644 --- a/packages/react-ui/src/lib/oauth2-utils.ts +++ b/packages/react-ui/src/lib/oauth2-utils.ts @@ -71,12 +71,20 @@ function constructUrl(params: OAuth2PopupParams, pckeChallenge: string) { return url.toString(); } -function getCode(redirectUrl: string): Promise { +function getCode(redirectUrl: string | undefined): Promise { return new Promise((resolve) => { window.addEventListener('message', function handler(event) { + if (!redirectUrl) { + return; + } + let origin; + try { + origin = new URL(redirectUrl).origin; + } catch (e) { + return; + } if ( - redirectUrl && - redirectUrl.startsWith(event.origin) && + origin === event.origin && event.data['code'] ) { resolve(decodeURIComponent(event.data.code)); diff --git a/packages/server/api/src/app/app.ts b/packages/server/api/src/app/app.ts index 671220419f..f5823c0446 100644 --- a/packages/server/api/src/app/app.ts +++ b/packages/server/api/src/app/app.ts @@ -250,12 +250,13 @@ export const setupApp = async (app: FastifyInstance): Promise = return reply.send('The code is missing in url') } else { + const origin = await domainHelper.getPublicUrl({}) return reply .type('text/html') .send( ` Redirect succuesfully, this window should close now`, + )}' }, '${origin}')} Redirect succuesfully, this window should close now`, ) } },