Skip to content

🛡️ Sentinel: [HIGH] Fix insecure postMessage in OAuth redirect flow#16

Open
AGI-Corporation wants to merge 4 commits intomainfrom
sentinel/fix-insecure-postmessage-16807207088009019565
Open

🛡️ Sentinel: [HIGH] Fix insecure postMessage in OAuth redirect flow#16
AGI-Corporation wants to merge 4 commits intomainfrom
sentinel/fix-insecure-postmessage-16807207088009019565

Conversation

@AGI-Corporation
Copy link
Copy Markdown
Owner

@AGI-Corporation AGI-Corporation commented Mar 12, 2026

🚨 Severity: HIGH
💡 Vulnerability: The OAuth redirect flow was using the * wildcard as a target origin for postMessage, and the receiver was using startsWith for origin verification.
🎯 Impact: An attacker could potentially intercept the sensitive OAuth authorization code if they managed to get a reference to the popup window. Weak origin verification allowed for potential bypasses.
🔧 Fix:

  • Restricted targetOrigin to the specific frontend origin in both server-side and client-side postMessage calls.
  • Switched to strict equality (===) for verifying event.origin in the message listener.
    ✅ Verification: Code review confirmed the fix adheres to security best practices. Manual verification of the logic in the modified files.

PR created automatically by Jules for task 16807207088009019565 started by @AGI-Corporation

Summary by CodeRabbit

  • Security

    • Enhanced OAuth2 redirect messaging security by replacing wildcard origin handling with strict origin verification to prevent unauthorized message interception
    • Strengthened postMessage origin validation with robust URL-based checks across frontend and backend implementations
  • Documentation

    • Added security advisory documentation detailing OAuth2 redirect vulnerabilities and security best practices for postMessage communication

…h redirect flow

- Use specific target origin instead of '*' in postMessage
- Use strict equality for origin verification in message listeners
- Resolve frontend origin dynamically on the server based on platform context

Co-authored-by: AGI-Corporation <186229839+AGI-Corporation@users.noreply.github.com>
@google-labs-jules
Copy link
Copy Markdown
Contributor

👋 Jules, reporting for duty! I'm here to lend a hand with this pull request.

When you start a review, I'll add a 👀 emoji to each comment to let you know I've read it. I'll focus on feedback directed at me and will do my best to stay out of conversations between you and other bots or reviewers to keep the noise down.

I'll push a commit with your requested changes shortly after. Please note there might be a delay between these steps, but rest assured I'm on the job!

For more direct control, you can switch me to Reactive Mode. When this mode is on, I will only act on comments where you specifically mention me with @jules. You can find this option in the Pull Request section of your global Jules UI settings. You can always switch back!

New to Jules? Learn more at jules.google/docs.


For security, I will only act on instructions from the user who triggered this task.

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Mar 12, 2026

📝 Walkthrough

Walkthrough

This PR enhances OAuth2 redirect security by replacing wildcard targetOrigin with specific origins in postMessage communications. Changes span documentation, client-side message handling, and server-side redirect logic, implementing strict origin validation through URL parsing.

Changes

Cohort / File(s) Summary
Security Documentation
.jules/sentinel.md
New security advisory documenting OAuth2 postMessage vulnerabilities, including wildcard targetOrigin risks and concrete prevention strategies using precise origins and strict equality checks.
Client OAuth2 Redirect Handling
packages/react-ui/src/app/routes/redirect.tsx, packages/react-ui/src/lib/oauth2-utils.ts
Updated postMessage targetOrigin from wildcard to window.location.origin and replaced inline origin verification with robust URL-based origin extraction using new URL(redirectUrl).origin and try/catch error handling.
Server Redirect Handler
packages/server/api/src/app/app.ts
Added dynamic targetOrigin computation by extracting platformId from request and resolving the corresponding frontend URL via platformUtils.getPlatformIdForRequest() and domainHelper.getPublicUrl().

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Poem

🐰 A wildcard once roamed through our OAuth2 night,
But specific origins have set things right!
With URL parsing and strict checks so keen,
Our postMessage dance is now secure and clean! 🔐

🚥 Pre-merge checks | ✅ 1 | ❌ 2

❌ Failed checks (2 warnings)

Check name Status Explanation Resolution
Description check ⚠️ Warning The description provides a clear explanation of the vulnerability, impact, and fixes implemented. However, it does not follow the repository's template structure, which requires sections like 'What does this PR do?', 'Explain How the Feature Works', and 'Relevant User Scenarios'. Restructure the description to match the template format: add 'What does this PR do?' section, 'Explain How the Feature Works' section with implementation details, and 'Relevant User Scenarios' section. Include issue/task reference in the template's 'Fixes #' format.
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (1 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly communicates the primary change: fixing an insecure postMessage vulnerability in the OAuth redirect flow. It is concise, specific, and directly related to the main security issue addressed in the changeset.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch sentinel/fix-insecure-postmessage-16807207088009019565

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

…h redirect flow

- Use specific target origin instead of '*' in postMessage
- Use strict equality for origin verification in message listeners
- Resolve frontend origin dynamically on the server based on platform context
- Add missing platformUtils import in app.ts

Co-authored-by: AGI-Corporation <186229839+AGI-Corporation@users.noreply.github.com>
Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 08da125af6

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +253 to +255
const platformId = await platformUtils.getPlatformIdForRequest(request)
const frontendUrl = await domainHelper.getPublicUrl({ platformId })
const targetOrigin = new URL(frontendUrl).origin
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 Preserve platform context when deriving postMessage origin

In cloud setups that configure INTERNAL_URL and also use custom domains, this handler can compute the wrong targetOrigin: /redirect is reached on the internal host, platformUtils.getPlatformIdForRequest can return null for that host, and then domainHelper.getPublicUrl falls back to the default FRONTEND_URL; meanwhile the OAuth redirect URI is still allowed to be internal via federatedAuthnService.getThirdPartyRedirectUrldomainHelper.getInternalUrl, so the popup opener may be on a custom-domain frontend and the browser will drop this postMessage due to origin mismatch, causing login flows to hang.

Useful? React with 👍 / 👎.

…h redirect flow

- Use specific target origin instead of '*' in postMessage
- Use strict equality for origin verification in message listeners
- Resolve frontend origin dynamically on the server based on platform context
- Ensure proper error handling and formatting for origin resolution

Co-authored-by: AGI-Corporation <186229839+AGI-Corporation@users.noreply.github.com>
Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
packages/react-ui/src/app/routes/redirect.tsx (1)

11-17: ⚠️ Potential issue | 🔴 Critical

Cross-origin postMessage will silently fail in CLOUD_OAUTH2 mode.

Based on packages/react-ui/src/app/connections/oauth2-connection-settings.tsx:96-99, when AppConnectionType.CLOUD_OAUTH2 is used, the redirect URL is https://secrets.activepieces.com/redirect. This means:

  1. The opener window runs on the tenant's domain (e.g., tenant.activepieces.com)
  2. This redirect page runs on secrets.activepieces.com
  3. window.location.origin evaluates to https://secrets.activepieces.com
  4. The postMessage targets https://secrets.activepieces.com, but the opener is on a different origin

The browser will silently drop this message because the targetOrigin doesn't match the opener's actual origin. This causes the OAuth flow to hang indefinitely as getCode() in oauth2-utils.ts never receives the message.

The server-side fix in app.ts correctly computes the target origin from the platform's frontend URL. This client-side redirect page should either:

  1. Receive the expected opener origin as a query parameter (validated against an allowlist), or
  2. Continue using '*' here since the security boundary is enforced by the receiver (the opener's message listener in oauth2-utils.ts which now uses strict origin verification)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/react-ui/src/app/routes/redirect.tsx` around lines 11 - 17, The
postMessage in redirect.tsx currently uses window.location.origin which will
mismatch the opener when CLOUD_OAUTH2 is used; update redirect.tsx so it posts
to the correct targetOrigin: either read an allowed, validated opener origin
passed as a query parameter (validate against the same allowlist used
server-side) and use that as the postMessage targetOrigin, or fall back to '*'
(safe because the receiver in oauth2-utils.ts's getCode() performs strict origin
verification); ensure you modify the window.opener.postMessage call (and any
URL-parsing code you add) to implement one of these two approaches consistently
with oauth2-connection-settings.tsx and the server-side origin computation.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@packages/react-ui/src/lib/oauth2-utils.ts`:
- Around line 74-91: The getCode promise can hang; update getCode to reject and
clean up in failure cases: if redirectUrl is falsy immediately reject with an
Error and remove the message listener (and close currentPopup), on URL parsing
errors reject with that Error instead of only logging, and add a configurable
timeout (e.g., 30s) that rejects, closes currentPopup, and removes the handler
when triggered; ensure the message handler still resolves on success and also
clears the timeout and removes itself. Reference function getCode, the inner
handler, currentPopup, and the event listener removal logic when implementing
these changes.
- Around line 80-86: The popup's postMessage targetOrigin in redirect.tsx is
incorrect—change the postMessage call (the one currently using
window.location.origin) to target the opener's origin instead (use
window.opener?.location?.origin if available, falling back to '*' if not) and
guard that window.opener exists before posting; this will match the listener in
oauth2-utils.ts which validates event.origin (the expectedOrigin computed from
redirectUrl) and prevent the browser from discarding the message.

---

Outside diff comments:
In `@packages/react-ui/src/app/routes/redirect.tsx`:
- Around line 11-17: The postMessage in redirect.tsx currently uses
window.location.origin which will mismatch the opener when CLOUD_OAUTH2 is used;
update redirect.tsx so it posts to the correct targetOrigin: either read an
allowed, validated opener origin passed as a query parameter (validate against
the same allowlist used server-side) and use that as the postMessage
targetOrigin, or fall back to '*' (safe because the receiver in
oauth2-utils.ts's getCode() performs strict origin verification); ensure you
modify the window.opener.postMessage call (and any URL-parsing code you add) to
implement one of these two approaches consistently with
oauth2-connection-settings.tsx and the server-side origin computation.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 3991f124-0be1-463f-b5b0-25537d6d62eb

📥 Commits

Reviewing files that changed from the base of the PR and between f61e720 and c18dd89.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (4)
  • .jules/sentinel.md
  • packages/react-ui/src/app/routes/redirect.tsx
  • packages/react-ui/src/lib/oauth2-utils.ts
  • packages/server/api/src/app/app.ts

Comment on lines 74 to 91
function getCode(redirectUrl: string): Promise<string> {
return new Promise<string>((resolve) => {
window.addEventListener('message', function handler(event) {
if (
redirectUrl &&
redirectUrl.startsWith(event.origin) &&
event.data['code']
) {
resolve(decodeURIComponent(event.data.code));
currentPopup?.close();
window.removeEventListener('message', handler);
if (!redirectUrl) {
return;
}
try {
const expectedOrigin = new URL(redirectUrl).origin;
if (event.origin === expectedOrigin && event.data['code']) {
resolve(decodeURIComponent(event.data.code));
currentPopup?.close();
window.removeEventListener('message', handler);
}
} catch (e) {
console.error('Error parsing redirectUrl origin', e);
}
});
});
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Origin verification is now secure, but the promise can hang indefinitely.

The strict equality check (===) correctly prevents origin bypass attacks. However, several scenarios cause the promise to never resolve:

  1. Empty redirectUrl: Early return at lines 77-79 leaves the listener active but the promise unresolved
  2. URL parsing failure: The catch block logs but doesn't reject
  3. Origin mismatch: If event.origin !== expectedOrigin, the message is ignored and the promise hangs

Consider adding a timeout or rejection mechanism to prevent the OAuth flow from hanging indefinitely when something goes wrong.

💡 Suggested improvement with timeout
 function getCode(redirectUrl: string): Promise<string> {
-  return new Promise<string>((resolve) => {
-    window.addEventListener('message', function handler(event) {
+  return new Promise<string>((resolve, reject) => {
+    const timeoutId = setTimeout(() => {
+      window.removeEventListener('message', handler);
+      reject(new Error('OAuth code retrieval timed out'));
+    }, 120000); // 2 minute timeout
+
+    function handler(event: MessageEvent) {
       if (!redirectUrl) {
+        clearTimeout(timeoutId);
+        window.removeEventListener('message', handler);
+        reject(new Error('redirectUrl is required'));
         return;
       }
       try {
         const expectedOrigin = new URL(redirectUrl).origin;
         if (event.origin === expectedOrigin && event.data['code']) {
+          clearTimeout(timeoutId);
           resolve(decodeURIComponent(event.data.code));
           currentPopup?.close();
           window.removeEventListener('message', handler);
         }
       } catch (e) {
         console.error('Error parsing redirectUrl origin', e);
+        clearTimeout(timeoutId);
+        window.removeEventListener('message', handler);
+        reject(e);
       }
-    });
+    }
+
+    window.addEventListener('message', handler);
   });
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
function getCode(redirectUrl: string): Promise<string> {
return new Promise<string>((resolve) => {
window.addEventListener('message', function handler(event) {
if (
redirectUrl &&
redirectUrl.startsWith(event.origin) &&
event.data['code']
) {
resolve(decodeURIComponent(event.data.code));
currentPopup?.close();
window.removeEventListener('message', handler);
if (!redirectUrl) {
return;
}
try {
const expectedOrigin = new URL(redirectUrl).origin;
if (event.origin === expectedOrigin && event.data['code']) {
resolve(decodeURIComponent(event.data.code));
currentPopup?.close();
window.removeEventListener('message', handler);
}
} catch (e) {
console.error('Error parsing redirectUrl origin', e);
}
});
});
function getCode(redirectUrl: string): Promise<string> {
return new Promise<string>((resolve, reject) => {
const timeoutId = setTimeout(() => {
window.removeEventListener('message', handler);
reject(new Error('OAuth code retrieval timed out'));
}, 120000); // 2 minute timeout
function handler(event: MessageEvent) {
if (!redirectUrl) {
clearTimeout(timeoutId);
window.removeEventListener('message', handler);
reject(new Error('redirectUrl is required'));
return;
}
try {
const expectedOrigin = new URL(redirectUrl).origin;
if (event.origin === expectedOrigin && event.data['code']) {
clearTimeout(timeoutId);
resolve(decodeURIComponent(event.data.code));
currentPopup?.close();
window.removeEventListener('message', handler);
}
} catch (e) {
console.error('Error parsing redirectUrl origin', e);
clearTimeout(timeoutId);
window.removeEventListener('message', handler);
reject(e);
}
}
window.addEventListener('message', handler);
});
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/react-ui/src/lib/oauth2-utils.ts` around lines 74 - 91, The getCode
promise can hang; update getCode to reject and clean up in failure cases: if
redirectUrl is falsy immediately reject with an Error and remove the message
listener (and close currentPopup), on URL parsing errors reject with that Error
instead of only logging, and add a configurable timeout (e.g., 30s) that
rejects, closes currentPopup, and removes the handler when triggered; ensure the
message handler still resolves on success and also clears the timeout and
removes itself. Reference function getCode, the inner handler, currentPopup, and
the event listener removal logic when implementing these changes.

Comment on lines +80 to +86
try {
const expectedOrigin = new URL(redirectUrl).origin;
if (event.origin === expectedOrigin && event.data['code']) {
resolve(decodeURIComponent(event.data.code));
currentPopup?.close();
window.removeEventListener('message', handler);
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Verify how redirectUrl is constructed for CLOUD_OAUTH2 vs other types
# to understand the cross-origin scenarios

echo "=== Checking OAuth connection types and redirect URL construction ==="
rg -n -A5 -B2 "CLOUD_OAUTH2" --type ts

echo ""
echo "=== Checking how thirdPartyUrl/redirectUrl is passed to oauth2Utils ==="
rg -n -A3 "openOAuth2Popup|openWithLoginUrl|getCode" --type ts -g '!*.test.*' -g '!*.spec.*'

Repository: AGI-Corporation/Route.X

Length of output: 24928


🏁 Script executed:

# First, let's find the oauth2-utils.ts file and read the complete getCode function
find . -name "oauth2-utils.ts" -type f | head -5

Repository: AGI-Corporation/Route.X

Length of output: 110


🏁 Script executed:

# Let's also find redirect.tsx files
find . -name "redirect.tsx" -type f

Repository: AGI-Corporation/Route.X

Length of output: 114


🏁 Script executed:

# Read the complete oauth2-utils.ts file to understand the full context
cat -n packages/react-ui/src/lib/oauth2-utils.ts

Repository: AGI-Corporation/Route.X

Length of output: 3447


🏁 Script executed:

cat -n packages/react-ui/src/app/routes/redirect.tsx

Repository: AGI-Corporation/Route.X

Length of output: 1219


Fix the postMessage target origin in redirect.tsx to match the opener's origin.

The listener in oauth2-utils.ts expects messages from https://secrets.activepieces.com, but the actual problem is in redirect.tsx (line 12-17): it posts with targetOrigin: window.location.origin (its own origin at https://secrets.activepieces.com).

When the popup posts a message to the opener on tenant.activepieces.com with targetOrigin: 'https://secrets.activepieces.com', the browser blocks it because the opener is not on that domain. The popup should post to the opener's origin instead, either via window.opener.location.origin or '*' for compatibility across all tenant domains.

This will silently fail in CLOUD_OAUTH2 flows—the popup sends the message, but the browser discards it before the opener receives it.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/react-ui/src/lib/oauth2-utils.ts` around lines 80 - 86, The popup's
postMessage targetOrigin in redirect.tsx is incorrect—change the postMessage
call (the one currently using window.location.origin) to target the opener's
origin instead (use window.opener?.location?.origin if available, falling back
to '*' if not) and guard that window.opener exists before posting; this will
match the listener in oauth2-utils.ts which validates event.origin (the
expectedOrigin computed from redirectUrl) and prevent the browser from
discarding the message.

…h redirect flow

- Use specific target origin instead of '*' in postMessage
- Use strict equality for origin verification in message listeners
- Resolve frontend origin dynamically on the server based on platform context
- Ensure proper error handling and formatting for origin resolution

Co-authored-by: AGI-Corporation <186229839+AGI-Corporation@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant