Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .jules/sentinel.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
## 2025-05-15 - [Secure OAuth Code Transmission]
**Vulnerability:** OAuth2 codes were sent to parent windows via `postMessage` using a wildcard (`'*'`) target origin, and the frontend used an insecure `startsWith` check for origin verification.
**Learning:** Monorepo architectures with multi-platform/tenant support (like Activepieces) require dynamic resolution of the frontend origin on the server to securely lock down `postMessage` targets.
**Prevention:** Always resolve the specific platform's base URL and extract its origin before sending sensitive data via `postMessage`. In the frontend, use strict equality (`===`) for origin comparison.
3 changes: 2 additions & 1 deletion packages/react-ui/src/lib/oauth2-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,10 +73,11 @@ function constructUrl(params: OAuth2PopupParams, pckeChallenge: string) {

function getCode(redirectUrl: string): Promise<string> {
return new Promise<string>((resolve) => {
const expectedOrigin = new URL(redirectUrl).origin;
window.addEventListener('message', function handler(event) {
if (
redirectUrl &&
redirectUrl.startsWith(event.origin) &&
event.origin === expectedOrigin &&
event.data['code']
) {
resolve(decodeURIComponent(event.data.code));
Expand Down
5 changes: 4 additions & 1 deletion packages/server/api/src/app/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -250,12 +250,15 @@ export const setupApp = async (app: FastifyInstance): Promise<FastifyInstance> =
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
Comment on lines +253 to +255
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Resolve postMessage target from redirect URI, not request host

targetOrigin is computed from platformUtils.getPlatformIdForRequest(request) and domainHelper.getPublicUrl(...), but the OAuth redirect URL is issued via federatedAuthnService.getThirdPartyRedirectUrl using domainHelper.getInternalUrl (which prefers INTERNAL_URL). When INTERNAL_URL is set, /redirect arrives on a shared host without tenant context, so getPlatformIdForRequest can return null and getPublicUrl falls back to the global frontend origin; for custom-domain tenants this origin differs from the popup opener, so window.opener.postMessage is discarded and the OAuth code never reaches the UI.

Useful? React with πŸ‘Β / πŸ‘Ž.

return reply
.type('text/html')
.send(
`<script>if(window.opener){window.opener.postMessage({ 'code': '${encodeURIComponent(
params.code,
)}' },'*')}</script> <html>Redirect succuesfully, this window should close now</html>`,
)}' }, '${targetOrigin}')}</script> <html>Redirect successfully, this window should close now</html>`,
)
}
},
Expand Down
Loading
Loading