Skip to content

Commit 70441b8

Browse files
committed
fix: add noopener,noreferrer to _blank targets to prevent reverse tabnabbing
Opening attacker-influenced app/preview URLs in a new tab without noopener leaves a live window.opener handle to the platform tab, enabling reverse tabnabbing (CWE-1022). Add noopener,noreferrer to all window.open('_blank') calls and rel=noopener noreferrer to target=_blank anchors that were missing it.
1 parent 453a4b0 commit 70441b8

7 files changed

Lines changed: 10 additions & 8 deletions

File tree

src/components/auth/login-modal.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -507,6 +507,7 @@ export function LoginModal({
507507
<a
508508
href="https://www.cloudflare.com/website-terms/"
509509
target="_blank"
510+
rel="noopener noreferrer"
510511
className="underline hover:text-kumo-default"
511512
>
512513
Terms of Service
@@ -515,6 +516,7 @@ export function LoginModal({
515516
<a
516517
href="https://www.cloudflare.com/privacypolicy/"
517518
target="_blank"
519+
rel="noopener noreferrer"
518520
className="underline hover:text-kumo-default"
519521
>
520522
Privacy Policy

src/components/cloudflare-account-selector.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -310,7 +310,7 @@ export function CloudflareAccountSelector() {
310310
<DropdownMenu.Content align="end">
311311
{gatewayDashUrl && (
312312
<DropdownMenu.Item
313-
onClick={() => window.open(gatewayDashUrl, '_blank')}
313+
onClick={() => window.open(gatewayDashUrl, '_blank', 'noopener,noreferrer')}
314314
>
315315
<ExternalLink className="size-4" />
316316
View gateway

src/features/presentation/components/PresentationHeaderActions.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ export function PresentationHeaderActions({
4848
: `${currentSrc}?print-pdf`;
4949

5050
// Open print view in new window for PDF export
51-
window.open(printUrl, '_blank');
51+
window.open(printUrl, '_blank', 'noopener,noreferrer');
5252
};
5353

5454
return (

src/routes/app/index.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -792,7 +792,7 @@ export default function AppView() {
792792
aria-label="Open in new tab"
793793
title="Open in new tab"
794794
onClick={() =>
795-
window.open(appUrl, '_blank')
795+
window.open(appUrl, '_blank', 'noopener,noreferrer')
796796
}
797797
icon={
798798
<ExternalLink className="size-3.5" />

src/routes/chat/chat.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -409,7 +409,7 @@ function ChatSession() {
409409
<Button
410410
variant="ghost"
411411
className="h-8 shrink-0 px-2 text-xs text-text-tertiary hover:bg-kumo-elevated hover:text-text-primary"
412-
onClick={() => window.open(cloudflareDeploymentUrl, '_blank')}
412+
onClick={() => window.open(cloudflareDeploymentUrl, '_blank', 'noopener,noreferrer')}
413413
>
414414
<ExternalLink className="size-3.5" />
415415
View Live

src/routes/chat/components/deployment-controls.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -123,7 +123,7 @@ export function DeploymentControls({
123123

124124
// Public apps (or missing appId): open the deployed URL directly.
125125
if (deploymentTarget === 'user' || localVisibility !== 'private' || !appId) {
126-
window.open(deploymentUrl, '_blank');
126+
window.open(deploymentUrl, '_blank', 'noopener,noreferrer');
127127
return;
128128
}
129129

@@ -132,15 +132,15 @@ export function DeploymentControls({
132132
try {
133133
const response = await apiClient.generatePreviewToken(appId);
134134
if (response.success && response.data) {
135-
window.open(response.data.previewUrl, '_blank');
135+
window.open(response.data.previewUrl, '_blank', 'noopener,noreferrer');
136136
return;
137137
}
138138
} catch (error) {
139139
console.error('Failed to generate owner preview token:', error);
140140
}
141141

142142
// Fallback: attempt the raw URL.
143-
window.open(deploymentUrl, '_blank');
143+
window.open(deploymentUrl, '_blank', 'noopener,noreferrer');
144144
};
145145

146146
const handleToggleVisibility = async () => {

src/utils/usage-limit-checker.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -156,7 +156,7 @@ function createInsufficientBalanceDialog(balance: number, accountId: string | un
156156
const url = accountId
157157
? `https://dash.cloudflare.com/${accountId}/ai/ai-gateway/credits`
158158
: 'https://dash.cloudflare.com';
159-
window.open(url, '_blank');
159+
window.open(url, '_blank', 'noopener,noreferrer');
160160
}}
161161
className="w-full sm:w-auto bg-brand-primary hover:bg-brand-primary/90 text-white"
162162
>

0 commit comments

Comments
 (0)