fix(frontend): resolve invalid HTML nesting and tabnabbing vulnerabilities (#1904)

* fix(frontend): resolve invalid HTML nesting and tabnabbing vulnerabilities

Fix `<button>` inside `<a>` invalid HTML in artifact components and add
missing `noopener,noreferrer` to `window.open` calls to prevent reverse
tabnabbing.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix(frontend): address Copilot review on tabnabbing and double-tab-open

Remove redundant parent onClick on web_fetch ChainOfThoughtStep to
prevent opening two tabs on link click, and explicitly null out
window.opener after window.open() for defensive tabnabbing hardening.

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
yangzheli 2026-04-07 09:44:17 +08:00 committed by GitHub
parent 2d068cc075
commit 3acdf79beb
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 45 additions and 40 deletions

View File

@ -188,17 +188,19 @@ export function ArtifactFileDetail({
</Tooltip> </Tooltip>
)} )}
{!isWriteFile && ( {!isWriteFile && (
<a <ArtifactAction
href={urlOfArtifact({ filepath, threadId })} icon={SquareArrowOutUpRightIcon}
target="_blank" label={t.common.openInNewWindow}
rel="noopener noreferrer" tooltip={t.common.openInNewWindow}
> onClick={() => {
<ArtifactAction const w = window.open(
icon={SquareArrowOutUpRightIcon} urlOfArtifact({ filepath, threadId }),
label={t.common.openInNewWindow} "_blank",
tooltip={t.common.openInNewWindow} "noopener,noreferrer",
/> );
</a> if (w) w.opener = null;
}}
/>
)} )}
{isCodeFile && ( {isCodeFile && (
<ArtifactAction <ArtifactAction
@ -218,17 +220,19 @@ export function ArtifactFileDetail({
/> />
)} )}
{!isWriteFile && ( {!isWriteFile && (
<a <ArtifactAction
href={urlOfArtifact({ filepath, threadId, download: true })} icon={DownloadIcon}
target="_blank" label={t.common.download}
rel="noopener noreferrer" tooltip={t.common.download}
> onClick={() => {
<ArtifactAction const w = window.open(
icon={DownloadIcon} urlOfArtifact({ filepath, threadId, download: true }),
label={t.common.download} "_blank",
tooltip={t.common.download} "noopener,noreferrer",
/> );
</a> if (w) w.opener = null;
}}
/>
)} )}
<ArtifactAction <ArtifactAction
icon={XIcon} icon={XIcon}

View File

@ -104,21 +104,21 @@ export function ArtifactFileList({
{t.common.install} {t.common.install}
</Button> </Button>
)} )}
<a <Button variant="ghost" asChild>
href={urlOfArtifact({ <a
filepath: file, href={urlOfArtifact({
threadId: threadId, filepath: file,
download: true, threadId: threadId,
})} download: true,
target="_blank" })}
rel="noopener noreferrer" target="_blank"
onClick={(e) => e.stopPropagation()} rel="noopener noreferrer"
> onClick={(e) => e.stopPropagation()}
<Button variant="ghost"> >
<DownloadIcon className="size-4" /> <DownloadIcon className="size-4" />
{t.common.download} {t.common.download}
</Button> </a>
</a> </Button>
</CardAction> </CardAction>
</CardHeader> </CardHeader>
</Card> </Card>

View File

@ -280,16 +280,17 @@ function ToolCall({
return ( return (
<ChainOfThoughtStep <ChainOfThoughtStep
key={id} key={id}
className="cursor-pointer"
label={t.toolCalls.viewWebPage} label={t.toolCalls.viewWebPage}
icon={GlobeIcon} icon={GlobeIcon}
onClick={() => {
window.open(url, "_blank");
}}
> >
<ChainOfThoughtSearchResult> <ChainOfThoughtSearchResult>
{url && ( {url && (
<a href={url} target="_blank" rel="noopener noreferrer"> <a
href={url}
target="_blank"
rel="noopener noreferrer"
className="cursor-pointer"
>
{title} {title}
</a> </a>
)} )}