Skip to content

fix(chat): escape attachment filename and validate file URL scheme to prevent XSS#5028

Merged
waleedlatif1 merged 1 commit into
stagingfrom
fix/chat-xss-escaping
Jun 13, 2026
Merged

fix(chat): escape attachment filename and validate file URL scheme to prevent XSS#5028
waleedlatif1 merged 1 commit into
stagingfrom
fix/chat-xss-escaping

Conversation

@waleedlatif1

Copy link
Copy Markdown
Collaborator

Summary

  • Route the public chat image-attachment preview through the existing escaped openAttachmentPreview helper (escapeHtml + blob URL); delete the unescaped document.write path that interpolated attachment.name and the data URL into an origin-inheriting about:blank (DOM XSS in the app origin)
  • Add isSafeHttpUrl (http/https allowlist, resolved via getBrowserOrigin) gating the file-download fallback window.open(file.url), blocking javascript:/data:/vbscript:/blob: schemes from agent-supplied URLs; also pass noopener,noreferrer
  • Add unit tests for escapeHtml (incl. markup-breakout payload) and isSafeHttpUrl (scheme allowlist + same-origin relative handling)

Type of Change

  • Bug fix

Testing

  • Added unit tests (10 passing) for the escaping and URL-scheme guards
  • Typecheck clean

Checklist

  • Code follows project style guidelines
  • Self-reviewed my changes
  • Tests added/updated and passing
  • No new warnings introduced
  • I confirm that I have read and agree to the terms outlined in the Contributor License Agreement (CLA)

@vercel

vercel Bot commented Jun 13, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
docs Skipped Skipped Jun 13, 2026 5:58pm

Request Review

@cursor

cursor Bot commented Jun 13, 2026

Copy link
Copy Markdown

PR Summary

Low Risk
Narrow, test-backed security hardening in chat UI; behavior change is tighter validation on preview and download fallback only.

Overview
Closes DOM XSS paths in public chat where untrusted attachment names/data URLs and agent-supplied file URLs could run script in the app origin.

Image attachment preview no longer uses document.write on an about:blank window with raw attachment.name and data URLs. Clicks now go through the shared openAttachmentPreview helper that escapeHtml-sanitizes the title/alt/src before opening a blob HTML preview with noopener,noreferrer. escapeHtml is exported for tests.

File download fallback (when serve/fetch fails) only calls window.open(file.url) if isSafeHttpUrl allows http/https (relative paths resolved via getBrowserOrigin), blocking javascript:, data:, blob:, etc.; same window hardening.

Adds Vitest coverage for escapeHtml (incl. markup-breakout filenames) and isSafeHttpUrl (scheme allowlist and edge cases).

Reviewed by Cursor Bugbot for commit bf85dd3. Configure here.

@waleedlatif1 waleedlatif1 merged commit 65c7029 into staging Jun 13, 2026
15 checks passed
@waleedlatif1 waleedlatif1 deleted the fix/chat-xss-escaping branch June 13, 2026 18:08
@greptile-apps

greptile-apps Bot commented Jun 13, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR fixes two DOM XSS vectors in the chat message UI: it removes a document.write path that interpolated unescaped attachment.name and raw data URLs into an origin-inheriting about:blank window, replacing it with a blob-URL approach where all user-controlled values pass through escapeHtml; and it adds an isSafeHttpUrl guard that rejects javascript:, data:, blob:, vbscript:, and file: schemes before the download-fallback window.open call, with noopener,noreferrer added throughout.

  • DOM XSS fix (message.tsx): The old inline openAttachmentPreview wrote raw attachment.name and validDataUrl directly into an about:blank page (same origin as the app); the replacement creates an HTML blob with both values passed through escapeHtml, opened via a blob URL with noopener,noreferrer.
  • URL scheme guard (file-download.tsx): isSafeHttpUrl uses new URL(url, getBrowserOrigin()) to resolve and inspect the protocol, ensuring only http: and https: schemes reach window.open; relative paths resolve to same-origin http and are intentionally allowed.
  • Tests: Ten unit tests cover escapeHtml (including a markup-breakout payload) and isSafeHttpUrl (scheme allowlist + relative-URL resolution semantics).

Confidence Score: 4/5

The two active XSS vectors are correctly closed; the new code is safe to ship.

The core fixes — replacing document.write with an escapeHtml-sanitized blob URL and adding a URL-scheme allowlist before window.open — are correctly implemented and well-tested. Two minor concerns keep this from a clean 5: the blob URL is revoked on a fixed 60-second timer rather than after confirmed page load, which can leave a user with a blank popup under load; and isSafeHttpUrl resolves relative strings to same-origin http and returns true, which is safe at the current call site but silently widens the contract beyond what the function name implies.

Both message.tsx and file-download.tsx are the critical files; the blob-URL revocation timing in message.tsx and the relative-URL acceptance behavior of isSafeHttpUrl in file-download.tsx are the two areas worth a second look before merge.

Important Files Changed

Filename Overview
apps/sim/app/chat/components/message/message.tsx Replaces unsafe document.write + about:blank preview (DOM XSS via unescaped attachment.name/dataUrl) with a blob-URL-based approach; escapeHtml is applied correctly in both title and attribute contexts. Minor: blob URL revocation timing relies on a fixed 60-second timeout.
apps/sim/app/chat/components/message/components/file-download.tsx Adds isSafeHttpUrl to block javascript:/data:/blob:/vbscript: schemes before window.open; adds noopener,noreferrer. Relative paths and empty strings resolve to same-origin http and return true — safe in the existing call site but potentially surprising contract for future callers.
apps/sim/app/chat/components/message/message.test.tsx New unit tests for escapeHtml covering all five HTML-significant characters, markup-breakout payloads, and entity double-encoding; correct and thorough.
apps/sim/app/chat/components/message/components/file-download.test.tsx New unit tests for isSafeHttpUrl covering http/https allow-list, javascript:/data:/vbscript:/blob:/file: rejection, and relative-URL resolution behavior; intentional edge cases are covered but semantics of '' and 'not a url' returning true are worth a comment.

Sequence Diagram

sequenceDiagram
    participant User
    participant ChatUI as message.tsx
    participant Preview as openAttachmentPreview()
    participant FD as file-download.tsx
    participant Browser

    User->>ChatUI: clicks image attachment
    ChatUI->>ChatUI: validate dataUrl starts with "data:"
    ChatUI->>Preview: openAttachmentPreview(name, dataUrl)
    Preview->>Preview: escapeHtml(name) → safeName
    Preview->>Preview: escapeHtml(dataUrl) → safeUrl
    Preview->>Preview: build HTML blob with safeName + safeUrl
    Preview->>Browser: URL.createObjectURL(blob)
    Preview->>Browser: window.open(blobUrl, "_blank", "noopener,noreferrer")
    Preview->>Browser: setTimeout → revokeObjectURL after 60s

    User->>FD: clicks file download button
    FD->>FD: triggerDownload(getFileUrl(file), file.name)
    alt download fails
        FD->>FD: isSafeHttpUrl(file.url)?
        FD->>FD: new URL(url, getBrowserOrigin())
        alt "protocol === http: or https:"
            FD->>Browser: window.open(file.url, "_blank", "noopener,noreferrer")
        else javascript: / data: / blob: / vbscript:
            FD->>FD: silently reject
        end
    end
Loading

Comments Outside Diff (1)

  1. apps/sim/app/chat/components/message/message.tsx, line 78-81 (link)

    P2 Blob URL may be revoked before the popup has loaded

    URL.revokeObjectURL(blobUrl) is scheduled 60 seconds after window.open is called, not after the page finishes loading. If the browser's popup-blocker defers opening the tab, the user dismisses-and-reopens the window manager, or the machine is under load, the blob URL can be gone before the page is fetched, leaving the user with an empty tab. Because the blob's entire HTML content (including the data: image in the src attribute) is baked into the blob at creation time, the page renders instantaneously once loaded — so the timeout is only a safety valve for memory, not for correctness. Switching to revoking from an onload listener on the new window, or simply relying on the garbage-collected blob lifetime, would eliminate the race.

Reviews (1): Last reviewed commit: "fix(chat): escape attachment filename an..." | Re-trigger Greptile

Comment on lines +63 to +70
export function isSafeHttpUrl(url: string): boolean {
try {
const parsed = new URL(url, getBrowserOrigin() ?? undefined)
return parsed.protocol === 'http:' || parsed.protocol === 'https:'
} catch {
return false
}
}

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.

P2 isSafeHttpUrl treats any relative-like string as safe

When getBrowserOrigin() returns a value (as it does in the browser), strings like '', 'not a url', or even ' ' resolve against the browser origin and come back with protocol http: or https:, so the function returns true. The calling site already guards against empty strings with if (file.url && ...), but a purely alphabetic or arbitrary non-URL string supplied by agent output (e.g. "download") would pass validation and be handed to window.open() as a relative path, navigating to <origin>/download in a new tab. This is harmless in isolation, but the function's contract advertises rejecting non-http(s) input, while the tests explicitly document the opposite. Consider adding an early if (!url.startsWith('http://') && !url.startsWith('https://') && !url.startsWith('/')) guard, or at minimum narrowing the test description so future maintainers understand the deliberate choice.

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.

1 participant