Skip to content

fix: add CSP headers to metadata proxy to prevent SVG XSS#919

Merged
Woody4618 merged 3 commits intosolana-foundation:masterfrom
hoodieshq:fix/svg-xss-metadata-proxy
Apr 7, 2026
Merged

fix: add CSP headers to metadata proxy to prevent SVG XSS#919
Woody4618 merged 3 commits intosolana-foundation:masterfrom
hoodieshq:fix/svg-xss-metadata-proxy

Conversation

@askov
Copy link
Copy Markdown
Contributor

@askov askov commented Apr 3, 2026

Description

  • Mitigate XSS via SVG served through the metadata proxy (/api/metadata/proxy?uri=...). An attacker-controlled SVG on IPFS, when navigated to directly (not via <img>), executes scripts on the Explorer's origin.
  • Add Content-Security-Policy: default-src 'none'; style-src 'unsafe-inline'; img-src data: and X-Content-Type-Options: nosniff to all proxy responses, blocking script execution while preserving cosmetic SVG
    rendering.

Type of change

  • Other (please describe): Security issue fix

Security references

Testing

  • Start dev server with NEXT_PUBLIC_METADATA_ENABLED=true
  • Temporarily remove CSP headers → navigate to proxy URL with SVG payload(self-crafted svg with a harmless script) → confirm
    script executes (vulnerable state)
  • Restore CSP headers → same URL → confirm CSP violation in DevTools console, no script execution
  • Verify token logos and NFT images still render via <img> tags

Related Issues

Closes HOO-339

Checklist

  • My code follows the project's style guidelines
  • All tests pass locally and in CI
  • CI/CD checks pass
  • For security-related features, I have included links to related information

@vercel
Copy link
Copy Markdown

vercel Bot commented Apr 3, 2026

@askov is attempting to deploy a commit to the Solana Foundation Team on Vercel.

A member of the Team first needs to authorize it.

@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps Bot commented Apr 3, 2026

Greptile Summary

This PR closes an XSS vulnerability in the metadata proxy (/api/metadata/proxy) where an attacker-controlled SVG hosted on IPFS could execute scripts on the Explorer's origin when the proxy URL was navigated to directly as a top-level document.

The fix adds two security headers to all successful proxy responses:

  • Content-Security-Policy: default-src 'none'; style-src 'unsafe-inline'; img-src data:; frame-ancestors 'none' — blocks script execution and network fetches while allowing inline CSS and embedded data-URI images (needed for SVG rendering), and prevents clickjacking via framing.
  • X-Content-Type-Options: nosniff — prevents the browser from MIME-sniffing the response into an executable type.

The policy is minimal and well-reasoned: default-src 'none' is the strongest possible baseline, style-src 'unsafe-inline' is needed to preserve SVG inline styling, and img-src data: allows data-URI embedded images in SVGs. frame-ancestors 'none' addresses the residual clickjacking surface noted in prior review. Error responses from respondWithError do not carry these headers, but those always return application/json and do not pose an XSS risk.

Confidence Score: 5/5

This PR is safe to merge — it adds strictly defensive headers with no functional regressions and the prior frame-ancestors concern is already addressed.

The change is purely additive (two response headers), targets a real demonstrated XSS vulnerability, uses correct and minimal CSP directives for the use case, and the previously raised frame-ancestors none concern has been incorporated. No P0 or P1 issues remain.

No files require special attention.

Important Files Changed

Filename Overview
app/api/metadata/proxy/route.ts Adds Content-Security-Policy and X-Content-Type-Options headers to all proxy success responses, blocking script execution when the proxy URL is opened directly as a document.

Sequence Diagram

sequenceDiagram
    participant Browser
    participant Proxy as "/api/metadata/proxy"
    participant Upstream as "Upstream (IPFS / HTTP)"

    Browser->>Proxy: "GET /api/metadata/proxy?uri=..."
    Proxy->>Upstream: "fetch(uri)"
    Upstream-->>Proxy: "200 OK (SVG / JSON / Binary)"
    Proxy-->>Browser: "200 OK with CSP + X-Content-Type-Options headers"

    Note over Browser: "SVG opened as top-level document: CSP blocks scripts, frame-ancestors blocks clickjacking"
    Note over Browser: "SVG loaded via img tag: scripts never execute anyway"
Loading

Reviews (2): Last reviewed commit: "fix: update CSP in metadata proxy for fr..." | Re-trigger Greptile

Comment thread app/api/metadata/proxy/route.ts Outdated
@askov
Copy link
Copy Markdown
Contributor Author

askov commented Apr 3, 2026

@greptile-apps review please

@vercel
Copy link
Copy Markdown

vercel Bot commented Apr 3, 2026

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

Project Deployment Actions Updated (UTC)
explorer Ready Ready Preview, Comment Apr 6, 2026 6:59am

Request Review

Comment thread app/api/metadata/proxy/route.ts Outdated
"default-src 'none'; style-src 'unsafe-inline'; img-src data:; frame-ancestors 'none'",
'Content-Type': resourceHeaders.get('content-type') ?? 'application/json; charset=utf-8',
Etag: resourceHeaders.get('etag') ?? 'no-etag',
'X-Content-Type-Options': 'nosniff',
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.

Let's maybe extract headers for the security cases into SECURITY_HEADERS const for better observability.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Good idea, extracted

Comment thread app/api/metadata/proxy/route.ts Outdated
// Prevent proxied content (e.g. SVG with embedded scripts) from executing
// anything if the proxy URL is opened directly as a top-level document.
'Content-Security-Policy':
"default-src 'none'; style-src 'unsafe-inline'; img-src data:; frame-ancestors 'none'",
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.

Suggested change
"default-src 'none'; style-src 'unsafe-inline'; img-src data:; frame-ancestors 'none'",
"default-src 'none'; style-src 'unsafe-inline'; img-src data:; frame-ancestors 'none'; sandbox",

I'd suggest adding sandbox headers as well to isolate the potentially harmful content from knowing where it is rendered

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

That is a good one, thanks

Copy link
Copy Markdown
Contributor

@rogaldh rogaldh left a comment

Choose a reason for hiding this comment

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

LGTM

Comment thread app/api/metadata/proxy/route.ts Outdated
'Cache-Control': resourceHeaders.get('cache-control') ?? 'no-cache',
// Prevent proxied content (e.g. SVG with embedded scripts) from executing
// anything if the proxy URL is opened directly as a top-level document.
'Content-Security-Policy':
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.

Suggested change
'Content-Security-Policy':
'Content-Disposition': 'attachment',
'Content-Security-Policy':
"default-src 'none'; frame-ancestors 'none'; sandbox;

It seems we can add 'Content-Disposition': 'attachment' header to the image and remove style-src and img-src headers. They'll cover cases with rendering for SVG's with malformed data

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I'm not sure it's worth it. All cases seem to be covered already, and most proxies I've encountered don't include 'attachment.' When they do, it can be a bit annoying because it makes debugging the response more difficult.

@askov askov force-pushed the fix/svg-xss-metadata-proxy branch from 1c1e9e8 to cf8b69b Compare April 6, 2026 06:26
Copy link
Copy Markdown
Contributor

@rogaldh rogaldh left a comment

Choose a reason for hiding this comment

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

LGTM

@rogaldh
Copy link
Copy Markdown
Contributor

rogaldh commented Apr 6, 2026

Partially unblocks #853

@Woody4618 Woody4618 merged commit ce5d135 into solana-foundation:master Apr 7, 2026
7 checks passed
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