diff --git a/.jules/sentinel.md b/.jules/sentinel.md new file mode 100644 index 0000000000..cf53f53450 --- /dev/null +++ b/.jules/sentinel.md @@ -0,0 +1,4 @@ +## 2025-05-15 - [HIGH] Insecure postMessage Origin in OAuth Redirects +**Vulnerability:** Use of wildcard origin (`'*'`) in `window.postMessage` calls within OAuth redirect handlers (both server-side generated HTML and client-side React routes). +**Learning:** Wildcard origins in `postMessage` allow any site that can open or iframe the redirect page to intercept sensitive information, such as OAuth authorization codes. In a multi-tenant environment (like Activepieces with custom domains), the expected origin must be resolved dynamically. +**Prevention:** Always restrict the `targetOrigin` to a trusted value. For client-side redirects, use `window.location.origin` if the communication is same-origin. For server-side generated redirects, use platform-aware URL resolvers (e.g., `domainHelper.getPublicUrl`) to determine the correct frontend origin. 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..2a99daace2 100644 --- a/packages/react-ui/src/lib/oauth2-utils.ts +++ b/packages/react-ui/src/lib/oauth2-utils.ts @@ -73,10 +73,18 @@ function constructUrl(params: OAuth2PopupParams, pckeChallenge: string) { function getCode(redirectUrl: string): Promise { return new Promise((resolve) => { + let expectedOrigin: string | null = null; + try { + expectedOrigin = redirectUrl ? new URL(redirectUrl).origin : null; + } catch (e) { + console.error('Invalid redirectUrl', redirectUrl, e); + } + window.addEventListener('message', function handler(event) { if ( redirectUrl && - redirectUrl.startsWith(event.origin) && + expectedOrigin && + event.origin === expectedOrigin && 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..f0627db6c7 100644 --- a/packages/server/api/src/app/app.ts +++ b/packages/server/api/src/app/app.ts @@ -82,6 +82,7 @@ import { pieceMetadataServiceHooks } from './pieces/piece-metadata-service/hooks import { pieceSyncService } from './pieces/piece-sync-service' import { platformModule } from './platform/platform.module' import { platformService } from './platform/platform.service' +import { platformUtils } from './platform/platform.utils' import { projectHooks } from './project/project-hooks' import { projectModule } from './project/project-module' import { storeEntryModule } from './store-entry/store-entry.module' @@ -250,12 +251,15 @@ export const setupApp = async (app: FastifyInstance): Promise = return reply.send('The code is missing in url') } else { + const platformId = await platformUtils.getPlatformIdForRequest(request) + const frontendUrl = await domainHelper.getPublicUrl({ platformId }) + const targetOrigin = new URL(frontendUrl).origin return reply .type('text/html') .send( ` Redirect succuesfully, this window should close now`, + )}' },'${targetOrigin}')} Redirect successfully, this window should close now`, ) } },