Skip to content

Commit 8edb247

Browse files
committed
fix(chat): target the manage readiness ping at a concrete parent origin
The embedded assistant announced readiness with a '*' wildcard postMessage target, which SonarQube flags as a CRITICAL vulnerability: any window origin could receive the ping. The Manage parent now forwards its own origin as a parentOrigin query param; the child validates it is a bare, well-formed origin and targets the ping there, and skips the proactive ping when the param is absent. The parent's existing retry burst still delivers the context in that case, so no behavior regresses.
1 parent eacb26e commit 8edb247

4 files changed

Lines changed: 46 additions & 4 deletions

File tree

apps/chat/src/hooks/useEmbeddedManageContext.ts

Lines changed: 25 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
'use client'
22

3+
import { useSearchParams } from 'next/navigation'
34
import { useEffect, useRef, useState } from 'react'
45
import {
56
sanitizeManageAssistantContext,
@@ -14,6 +15,8 @@ const MANAGE_CONTEXT_READY_MESSAGE_TYPE = 'klicker:manage-context-ready'
1415

1516
export function useEmbeddedManageContext() {
1617
const embedded = useEmbedded()
18+
const searchParams = useSearchParams()
19+
const parentOrigin = parseOrigin(searchParams.get('parentOrigin'))
1720
const [context, setContext] = useState<ManageAssistantContext | null>(null)
1821
const contextKeyRef = useRef<string | null>(null)
1922
const setManageParentOrigin = useManageParentStore(
@@ -70,12 +73,19 @@ export function useEmbeddedManageContext() {
7073
// Announce readiness so the parent (re)sends the current context exactly
7174
// when this listener exists. Without this, the parent's timed retry burst
7275
// can fully elapse before hydration finishes and the context is lost. The
73-
// '*' target is safe: the message is a content-free ping and the parent
74-
// origin is unknown until its first context message arrives.
75-
window.parent.postMessage({ type: MANAGE_CONTEXT_READY_MESSAGE_TYPE }, '*')
76+
// parent hands us its own origin as a query param, so target the ping at
77+
// that concrete origin; if it is absent, skip the proactive ping and let
78+
// the parent's retry burst deliver the context rather than broadcasting to
79+
// a '*' wildcard.
80+
if (parentOrigin) {
81+
window.parent.postMessage(
82+
{ type: MANAGE_CONTEXT_READY_MESSAGE_TYPE },
83+
parentOrigin
84+
)
85+
}
7686

7787
return () => window.removeEventListener('message', handleMessage)
78-
}, [embedded, setManageParentOrigin])
88+
}, [embedded, parentOrigin, setManageParentOrigin])
7989

8090
return context
8191
}
@@ -91,3 +101,14 @@ function isManageContextMessage(data: unknown): data is {
91101
(data as { type?: unknown }).type === MANAGE_CONTEXT_MESSAGE_TYPE
92102
)
93103
}
104+
105+
// Accept the query param only when it is a bare, well-formed origin so it can
106+
// never be a '*' wildcard or a full URL when used as a postMessage target.
107+
function parseOrigin(value: string | null): string | null {
108+
if (!value) return null
109+
try {
110+
return new URL(value).origin === value ? value : null
111+
} catch {
112+
return null
113+
}
114+
}

apps/frontend-manage/src/components/assistant/ManageAssistantWidget.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,8 @@ export function ManageAssistantWidget() {
5252
buildManageAssistantUrl({
5353
chatUrl: process.env.NEXT_PUBLIC_CHAT_URL,
5454
locale: router.locale,
55+
parentOrigin:
56+
typeof window !== 'undefined' ? window.location.origin : undefined,
5557
}),
5658
[router.locale]
5759
)

apps/frontend-manage/src/components/assistant/manageAssistantConfig.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
type BuildManageAssistantUrlArgs = {
22
chatUrl?: string
33
locale?: string
4+
parentOrigin?: string
45
}
56

67
export function isManageAssistantEnabled(value: string | undefined): boolean {
@@ -10,6 +11,7 @@ export function isManageAssistantEnabled(value: string | undefined): boolean {
1011
export function buildManageAssistantUrl({
1112
chatUrl,
1213
locale,
14+
parentOrigin,
1315
}: BuildManageAssistantUrlArgs): string | null {
1416
if (!chatUrl) return null
1517

@@ -21,6 +23,12 @@ export function buildManageAssistantUrl({
2123
url.searchParams.set('locale', locale)
2224
}
2325

26+
// Hand the embedder's own origin to the embedded assistant so its
27+
// readiness ping can target a concrete origin instead of a '*' wildcard.
28+
if (parentOrigin) {
29+
url.searchParams.set('parentOrigin', parentOrigin)
30+
}
31+
2432
return url.toString()
2533
} catch {
2634
return null

apps/frontend-manage/test/manageAssistantConfig.test.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,17 @@ assert.equal(
1818
'https://chat.klicker.com/manage?embed=true&locale=de'
1919
)
2020

21+
// The embedder origin is forwarded so the embedded assistant can target its
22+
// readiness ping at a concrete origin instead of a '*' wildcard.
23+
assert.equal(
24+
buildManageAssistantUrl({
25+
chatUrl: 'https://chat.klicker.com/',
26+
locale: 'de',
27+
parentOrigin: 'https://manage.klicker.com',
28+
}),
29+
'https://chat.klicker.com/manage?embed=true&locale=de&parentOrigin=https%3A%2F%2Fmanage.klicker.com'
30+
)
31+
2132
assert.equal(
2233
buildManageAssistantUrl({
2334
chatUrl: undefined,

0 commit comments

Comments
 (0)