Skip to content

fix: validate origin and source on Klaviyo OAuth message handler [] - #11293

Merged
Michael Pineiro (michaelpineirocontentful) merged 3 commits into
masterfrom
fix/klaviyo-oauth-message-handler-origin-check
Sep 14, 2026
Merged

Michael Pineiro (michaelpineirocontentful) merged 3 commits into
masterfrom
fix/klaviyo-oauth-message-handler-origin-check

Conversation

@michaelpineirocontentful

Copy link
Copy Markdown
Contributor

Summary

The window message listener that completes the Klaviyo OAuth flow accepts any message event delivered to the window, without checking where it came from or which window sent it. Combined with a same-origin sender, this closes the loop on validating the full message channel in both directions.

Solution

  • Reject any message in the OAuth completion handler that doesn't come from the app's own origin, or that doesn't come from the specific popup window this instance opened.

Context

Stacked on top of the popup-side origin fix — this PR is the receiving half of the same channel. See that PR for the broader context on this stack.

Test plan

  • tsc --noEmit passes
  • Frontend build (vite build) succeeds
  • Manual OAuth connect flow verified in a staging environment

@michaelpineirocontentful Michael Pineiro (michaelpineirocontentful) changed the title fix: validate origin and source on Klaviyo OAuth message handler fix: validate origin and source on Klaviyo OAuth message handler [] Sep 11, 2026
@michaelpineirocontentful
Michael Pineiro (michaelpineirocontentful) force-pushed the fix/klaviyo-oauth-message-handler-origin-check branch from bcd963b to 1000f1b Compare September 11, 2026 15:08
@michaelpineirocontentful
Michael Pineiro (michaelpineirocontentful) marked this pull request as ready for review September 11, 2026 15:09

@david-shibley-contentful david-shibley-contentful left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

good call

@michaelpineirocontentful
Michael Pineiro (michaelpineirocontentful) removed this pull request from stack #11296 September 14, 2026 14:09
Base automatically changed from fix/klaviyo-oauth-popup-target-origin to master September 14, 2026 14:09
…w messages

The window message listener that completes the OAuth flow accepted
any message posted to the window, regardless of where it came from
or which window sent it.

Reject messages that don't come from the app's own origin, or that
don't come from the popup window this instance actually opened.
@michaelpineirocontentful
Michael Pineiro (michaelpineirocontentful) force-pushed the fix/klaviyo-oauth-message-handler-origin-check branch from 1000f1b to 813a28c Compare September 14, 2026 14:09
#11294)

* fix(klaviyo): scope OAuth popup message to the app's own origin

The OAuth callback popup broadcast its completion message with a
wildcard target origin, so any window that ended up as window.opener
could receive the authorization code and state.

Scope the postMessage call to the app's own origin instead of '*'.

* fix(klaviyo): validate origin and source before acting on OAuth window messages

The window message listener that completes the OAuth flow accepted
any message posted to the window, regardless of where it came from
or which window sent it.

Reject messages that don't come from the app's own origin, or that
don't come from the popup window this instance actually opened.

* fix(klaviyo): anchor proxy endpoint allowlist to the full path shape

The proxy's endpoint allowlist only checked the segment before the
first slash, so a value like "template-universal-content/../lists"
passed the check while the traversal segment let the request reach a
Klaviyo endpoint that was never allowlisted.

Replace the prefix check with a pattern anchored to the exact shape
the app actually sends (an allowed endpoint, optionally followed by a
single id segment), rejecting anything else outright.

* test(klaviyo): add unit tests for the proxy endpoint allowlist

Covers exact allowlist matches, an allowlisted endpoint plus a single
id segment, and traversal attempts that previously bypassed the
prefix-only check (verified these fail against the pre-fix code and
pass against the anchored-pattern fix).

Widen the vitest include glob to pick up functions/ tests — the
functions/ directory previously had no test coverage at all.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* style(klaviyo): run prettier on the proxy allowlist test

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* refactor(klaviyo): drop the now-unused ALLOWED_ENDPOINTS array

The allowlist check moved to ALLOWED_ENDPOINT_PATTERN; ALLOWED_ENDPOINTS
only survived as an internal building block for that regex. Inline it
directly into the pattern literal instead of keeping a separate binding
nothing else reads.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
* fix(klaviyo): scope OAuth popup message to the app's own origin

The OAuth callback popup broadcast its completion message with a
wildcard target origin, so any window that ended up as window.opener
could receive the authorization code and state.

Scope the postMessage call to the app's own origin instead of '*'.

* fix(klaviyo): validate origin and source before acting on OAuth window messages

The window message listener that completes the OAuth flow accepted
any message posted to the window, regardless of where it came from
or which window sent it.

Reject messages that don't come from the app's own origin, or that
don't come from the popup window this instance actually opened.

* fix(klaviyo): anchor proxy endpoint allowlist to the full path shape

The proxy's endpoint allowlist only checked the segment before the
first slash, so a value like "template-universal-content/../lists"
passed the check while the traversal segment let the request reach a
Klaviyo endpoint that was never allowlisted.

Replace the prefix check with a pattern anchored to the exact shape
the app actually sends (an allowed endpoint, optionally followed by a
single id segment), rejecting anything else outright.

* test(klaviyo): add unit tests for the proxy endpoint allowlist

Covers exact allowlist matches, an allowlisted endpoint plus a single
id segment, and traversal attempts that previously bypassed the
prefix-only check (verified these fail against the pre-fix code and
pass against the anchored-pattern fix).

Widen the vitest include glob to pick up functions/ tests — the
functions/ directory previously had no test coverage at all.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* style(klaviyo): run prettier on the proxy allowlist test

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* refactor(klaviyo): drop the now-unused ALLOWED_ENDPOINTS array

The allowlist check moved to ALLOWED_ENDPOINT_PATTERN; ALLOWED_ENDPOINTS
only survived as an internal building block for that regex. Inline it
directly into the pattern literal instead of keeping a separate binding
nothing else reads.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* fix(klaviyo): stop writing the bearer token to function logs

The Klaviyo request helper logged the full outgoing request headers
(including the Authorization bearer token it had just set) whenever
the API returned a non-OK response, and logged the raw token object
on the invalid-token-format branch as well.

Strip the Authorization header before logging request headers, and
log only the token's type instead of the token value on the
invalid-format error path.

* test(klaviyo): add unit tests for token scrubbing in makeRequest

Covers the two paths that previously leaked a secret to console.error:
a non-OK API response (which logged the full request headers including
Authorization) and an unrecognized token shape from the OAuth SDK
(which logged the raw token object). Verified both fail against the
pre-fix code and pass against the fix, and added a third test
confirming the fix still logs enough to debug an API error (status,
body, URL) without the secret.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* style(klaviyo): run prettier on the token-scrubbing test

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
@michaelpineirocontentful
Michael Pineiro (michaelpineirocontentful) merged commit 299919a into master Sep 14, 2026
15 checks passed
@michaelpineirocontentful
Michael Pineiro (michaelpineirocontentful) deleted the fix/klaviyo-oauth-message-handler-origin-check branch September 14, 2026 15:52
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.

3 participants