95c6a948 Fix origin validation bypass in postMessage trust checks (#4724)
## Description
Fixes a security issue reported by a customer where `postMessage`
origin/referrer validation could be bypassed using spoofed or lookalike
hostnames, potentially allowing untrusted windows to send messages
(including block bindings that get executed as code) to Builder-rendered
pages.
Specifically:
- **`packages/core/src/builder.class.ts`**:
`Builder.isTrustedHostForEvent` previously parsed the origin without
strictly validating the protocol/format, and `Builder.isEditing` used an
unanchored regex (`/builder\.io|localhost:1234/`) against
`document.referrer`, which would match spoofed hostnames like
`evil-builder.io.attacker.com` or `notlocalhost:1234x`. This has been
fixed by:
- Requiring the origin to match `^https?:\/\//i` before parsing.
- Validating the parsed protocol is `http:`/`https:` and the hostname is
trusted via `Builder.isTrustedHost`.
- Replacing the unanchored regex check in `isEditing` with a call to
`Builder.isTrustedHostForEvent`, reusing the same validated logic.
- **`packages/react/src/components/builder-block.component.tsx`**:
`BuilderBlock.onWindowMessage` did not validate the message origin at
all, unlike `BuilderContent.onWindowMessage`. Added an origin check
using `Builder.isTrustedHostForEvent(event)` at the top of the handler,
returning early if the origin isn't trusted, matching the existing
pattern used by `BuilderContent`.
- **`packages/sdks/src/functions/is-from-trusted-host.ts`**:
`isFromTrustedHost` used `startsWith('http')`/`startsWith('https')`
checks and did not guard against malformed URLs throwing during `new
URL(...)`. Updated to safely parse the URL in a try/catch, return
`false` on parse failure, and explicitly validate the protocol is
`http:` or `https:` before checking the hostname against the trusted
hosts list. This applies the same fix to the Gen2 SDKs.
### Tests
- Added/updated tests in `packages/core/src/builder.class.test.ts`,
`packages/react/test/builder-block.test.tsx` (new), and
`packages/sdks/src/functions/is-from-trusted-host.test.ts` covering
spoofed hostnames (e.g. `evil-builder.io.attacker.com`,
`notlocalhost:1234x`), non-HTTP origins (e.g.
`javascript://builder.io`), the literal string `"null"`, and non-URL
strings — asserting these are all correctly rejected as untrusted.
**JIRA**
https://builder-io.atlassian.net/browse/ENG-13357
---
<a
href="https://builder.io/app/projects/b29d4bc79bfe480fb3f0/classic-seed-fvi4jqvb"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://cdn.builder.io/api/v1/image/assets%2FYJIGb4i01jvw0SRdL5Bt%2F226fa21c49ce4f95a5aba53aa594fe7a"><img
src="https://cdn.builder.io/api/v1/image/assets%2FYJIGb4i01jvw0SRdL5Bt%2F949e3db6dedf4252bf6ae0258f4a37de"
alt="Edit in Builder"></picture></a> <a
href="https://b29d4bc79bfe480fb3f0-classic-seed-fvi4jqvb.projects.builder.my/"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://cdn.builder.io/api/v1/image/assets%2FYJIGb4i01jvw0SRdL5Bt%2Fe530b1333b5b4cedac9c41b8573c8268"><img
src="https://cdn.builder.io/api/v1/image/assets%2FYJIGb4i01jvw0SRdL5Bt%2Fbf5aebbec0b448779c805d58bacf6278"
alt="Preview"></picture></a>
---
To clone this PR locally use the [Github CLI](https://cli.github.com/)
with command `gh pr checkout 4724`
You can tag me at @builderio for anything you want me to fix or change
<!-- DO NOT EDIT THE CONTENT BELOW: -->
<!--<projectId>b29d4bc79bfe480fb3f0</projectId>-->
<!--<branchName>classic-seed-fvi4jqvb</branchName>-->
<!-- CURSOR_SUMMARY -->
---
> [!NOTE]
> **High Risk**
> Security fix for editor postMessage handling and code-binding updates;
incorrect validation could break legitimate editing or leave XSS vectors
open.
>
> **Overview**
> Closes a **postMessage origin bypass** where spoofed hostnames or
malformed origins could be treated as trusted, allowing untrusted pages
to send editor messages (including `builder.patchUpdates` that apply
bindings executed as code).
>
> **`Builder.isTrustedHostForEvent`** now requires `^https?://`, parses
the origin in a try/catch, enforces `http:`/`https:` protocol, and
checks the hostname with **`Builder.isTrustedHost`** (exact match, not
substring). **`Builder.isEditing`** drops the unanchored
`/builder\.io|localhost:1234/` referrer regex in favor of the same
helper.
>
> **`BuilderBlock.onWindowMessage`** adds an early
**`isTrustedHostForEvent`** guard (aligned with other editor listeners).
Gen2 **`isFromTrustedHost`** mirrors the same parse/protocol/hostname
rules.
>
> Tests cover lookalike domains, non-HTTP schemes, and invalid origins;
a changeset patches all affected SDK packages.
>
> <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit
e7f5de7509ad1c674bbb0279c1e95257d2cf64d9. Bugbot is set up for automated
code reviews on this repo. Configure
[here](https://www.cursor.com/dashboard/bugbot).</sup>
<!-- /CURSOR_SUMMARY -->
---------
Co-authored-by: Builder.io <builder-bot@builder.io> 1830e03c feat: ENG-12261 add OAuth 2.0 support and settings UI for Phrase integration (#4699)
## Description
Adds OAuth 2.0 support and settings UI for Phrase integration
<!-- CURSOR_SUMMARY -->
---
> [!NOTE]
> **Medium Risk**
> Changes authentication and how Phrase API calls are authorized (OAuth
popup, session state, 401 refresh), which can break translation
workflows if server OAuth endpoints or token refresh behave differently
than password auth.
>
> **Overview**
> Adds **SSO / OAuth 2.0** as an alternative to username/password for
the Phrase connector, aimed at orgs that block password login. Settings
gain an **Authentication** dropdown, per-org **OAuth Client ID**, US
data-center flag, and a custom **Connect / Disconnect** panel that runs
the Builder API OAuth popup flow (`prepare` → `start` → `postMessage`);
tokens stay server-side and the browser only sees connection metadata.
>
> Translation actions now go through a new **`PhraseApi`** wrapper that
checks credentials (`ensureAuthenticated`), retries on **401** via
**`oauth/refresh`** in OAuth mode, and handles org switches and stale
in-memory OAuth state with **apiKey-scoped session markers**.
Username/password fields are shown only when not in OAuth mode; both
credential types can coexist when switching modes.
>
> The README documents OAuth setup (Phrase registered app, redirect URI,
admin connect steps) and a sequence diagram. Plugin version bumps to
**0.0.17**. The stub **Request an updated translation** content action
is removed; pending-job **outdated source** warnings are refactored into
`checkTranslationFreshness`.
>
> <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit
5e0c959ba6cf03cb165653deb827b7723332a959. Bugbot is set up for automated
code reviews on this repo. Configure
[here](https://www.cursor.com/dashboard/bugbot).</sup>
<!-- /CURSOR_SUMMARY --> 270f73a7 feat: ENG-12261 add OAuth 2.0 support and settings UI for Phrase integration (#4699)
## Description
Adds OAuth 2.0 support and settings UI for Phrase integration
<!-- CURSOR_SUMMARY -->
---
> [!NOTE]
> **Medium Risk**
> Changes authentication and how Phrase API calls are authorized (OAuth
popup, session state, 401 refresh), which can break translation
workflows if server OAuth endpoints or token refresh behave differently
than password auth.
>
> **Overview**
> Adds **SSO / OAuth 2.0** as an alternative to username/password for
the Phrase connector, aimed at orgs that block password login. Settings
gain an **Authentication** dropdown, per-org **OAuth Client ID**, US
data-center flag, and a custom **Connect / Disconnect** panel that runs
the Builder API OAuth popup flow (`prepare` → `start` → `postMessage`);
tokens stay server-side and the browser only sees connection metadata.
>
> Translation actions now go through a new **`PhraseApi`** wrapper that
checks credentials (`ensureAuthenticated`), retries on **401** via
**`oauth/refresh`** in OAuth mode, and handles org switches and stale
in-memory OAuth state with **apiKey-scoped session markers**.
Username/password fields are shown only when not in OAuth mode; both
credential types can coexist when switching modes.
>
> The README documents OAuth setup (Phrase registered app, redirect URI,
admin connect steps) and a sequence diagram. Plugin version bumps to
**0.0.17**. The stub **Request an updated translation** content action
is removed; pending-job **outdated source** warnings are refactored into
`checkTranslationFreshness`.
>
> <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit
5e0c959ba6cf03cb165653deb827b7723332a959. Bugbot is set up for automated
code reviews on this repo. Configure
[here](https://www.cursor.com/dashboard/bugbot).</sup>
<!-- /CURSOR_SUMMARY --> 1830e03c feat: ENG-12261 add OAuth 2.0 support and settings UI for Phrase integration (#4699)
## Description
Adds OAuth 2.0 support and settings UI for Phrase integration
<!-- CURSOR_SUMMARY -->
---
> [!NOTE]
> **Medium Risk**
> Changes authentication and how Phrase API calls are authorized (OAuth
popup, session state, 401 refresh), which can break translation
workflows if server OAuth endpoints or token refresh behave differently
than password auth.
>
> **Overview**
> Adds **SSO / OAuth 2.0** as an alternative to username/password for
the Phrase connector, aimed at orgs that block password login. Settings
gain an **Authentication** dropdown, per-org **OAuth Client ID**, US
data-center flag, and a custom **Connect / Disconnect** panel that runs
the Builder API OAuth popup flow (`prepare` → `start` → `postMessage`);
tokens stay server-side and the browser only sees connection metadata.
>
> Translation actions now go through a new **`PhraseApi`** wrapper that
checks credentials (`ensureAuthenticated`), retries on **401** via
**`oauth/refresh`** in OAuth mode, and handles org switches and stale
in-memory OAuth state with **apiKey-scoped session markers**.
Username/password fields are shown only when not in OAuth mode; both
credential types can coexist when switching modes.
>
> The README documents OAuth setup (Phrase registered app, redirect URI,
admin connect steps) and a sequence diagram. Plugin version bumps to
**0.0.17**. The stub **Request an updated translation** content action
is removed; pending-job **outdated source** warnings are refactored into
`checkTranslationFreshness`.
>
> <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit
5e0c959ba6cf03cb165653deb827b7723332a959. Bugbot is set up for automated
code reviews on this repo. Configure
[here](https://www.cursor.com/dashboard/bugbot).</sup>
<!-- /CURSOR_SUMMARY --> 661064a3 feat: ENG-12261 add OAuth 2.0 support and settings UI for Phrase integration (#4699)
## Description
Adds OAuth 2.0 support and settings UI for Phrase integration
<!-- CURSOR_SUMMARY -->
---
> [!NOTE]
> **Medium Risk**
> Changes authentication and how Phrase API calls are authorized (OAuth
popup, session state, 401 refresh), which can break translation
workflows if server OAuth endpoints or token refresh behave differently
than password auth.
>
> **Overview**
> Adds **SSO / OAuth 2.0** as an alternative to username/password for
the Phrase connector, aimed at orgs that block password login. Settings
gain an **Authentication** dropdown, per-org **OAuth Client ID**, US
data-center flag, and a custom **Connect / Disconnect** panel that runs
the Builder API OAuth popup flow (`prepare` → `start` → `postMessage`);
tokens stay server-side and the browser only sees connection metadata.
>
> Translation actions now go through a new **`PhraseApi`** wrapper that
checks credentials (`ensureAuthenticated`), retries on **401** via
**`oauth/refresh`** in OAuth mode, and handles org switches and stale
in-memory OAuth state with **apiKey-scoped session markers**.
Username/password fields are shown only when not in OAuth mode; both
credential types can coexist when switching modes.
>
> The README documents OAuth setup (Phrase registered app, redirect URI,
admin connect steps) and a sequence diagram. Plugin version bumps to
**0.0.17**. The stub **Request an updated translation** content action
is removed; pending-job **outdated source** warnings are refactored into
`checkTranslationFreshness`.
>
> <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit
5e0c959ba6cf03cb165653deb827b7723332a959. Bugbot is set up for automated
code reviews on this repo. Configure
[here](https://www.cursor.com/dashboard/bugbot).</sup>
<!-- /CURSOR_SUMMARY --> 1830e03c feat: ENG-12261 add OAuth 2.0 support and settings UI for Phrase integration (#4699)
## Description
Adds OAuth 2.0 support and settings UI for Phrase integration
<!-- CURSOR_SUMMARY -->
---
> [!NOTE]
> **Medium Risk**
> Changes authentication and how Phrase API calls are authorized (OAuth
popup, session state, 401 refresh), which can break translation
workflows if server OAuth endpoints or token refresh behave differently
than password auth.
>
> **Overview**
> Adds **SSO / OAuth 2.0** as an alternative to username/password for
the Phrase connector, aimed at orgs that block password login. Settings
gain an **Authentication** dropdown, per-org **OAuth Client ID**, US
data-center flag, and a custom **Connect / Disconnect** panel that runs
the Builder API OAuth popup flow (`prepare` → `start` → `postMessage`);
tokens stay server-side and the browser only sees connection metadata.
>
> Translation actions now go through a new **`PhraseApi`** wrapper that
checks credentials (`ensureAuthenticated`), retries on **401** via
**`oauth/refresh`** in OAuth mode, and handles org switches and stale
in-memory OAuth state with **apiKey-scoped session markers**.
Username/password fields are shown only when not in OAuth mode; both
credential types can coexist when switching modes.
>
> The README documents OAuth setup (Phrase registered app, redirect URI,
admin connect steps) and a sequence diagram. Plugin version bumps to
**0.0.17**. The stub **Request an updated translation** content action
is removed; pending-job **outdated source** warnings are refactored into
`checkTranslationFreshness`.
>
> <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit
5e0c959ba6cf03cb165653deb827b7723332a959. Bugbot is set up for automated
code reviews on this repo. Configure
[here](https://www.cursor.com/dashboard/bugbot).</sup>
<!-- /CURSOR_SUMMARY -->