feat(security): add CSP + validate third-party thumbnail + encode id - #34
Merged
Merged
Conversation
Defense-in-depth hardening (no active vuln — the app is keyless and renders all external strings as escaped React text): - Content-Security-Policy: injected into index.html for PRODUCTION BUILDS ONLY (dev/HMR needs inline+eval) via a small Vite plugin. Locks default-src to 'self', restricts img to YouTube's CDNs, connect to noembed+lrclib, media to self+blob, and forbids object/base-uri. style-src keeps 'unsafe-inline' for React's inline style attributes (never for script). - YouTubeLinkCard: the thumbnail URL comes from noembed's response, so validate it (https + a ytimg/ggpht host) before using it as an <img src>; drop it otherwise (the card still shows title/author). - youtube.js: encodeURIComponent the id in the noembed request URL — a no-op for a valid 11-char id, but prevents query-param injection if an unvalidated id ever reaches it. Verified against the PRODUCTION build served under /melody/: app renders, styles/fonts apply, media blob: URLs play, and there are zero CSP violations or console errors. Fixes #18. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FkDKh1Uo1a4D7n5wCKdcKF
Review follow-ups: inject the CSP meta as the first head child so it governs the app bundle/CSS/font fetches (a meta CSP doesn't apply to subresources requested before it's parsed); and require a *.ytimg.com / *.ggpht.com subdomain in safeThumb to match the img-src allowlist (the CSP doesn't permit the bare apex). Re-verified against the production build: renders, styles/fonts apply, zero CSP violations. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FkDKh1Uo1a4D7n5wCKdcKF
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #18.
Defense-in-depth hardening (no active vuln — the app is keyless and renders all external strings as escaped React text).
Changes
index.htmlfor production builds only (dev/HMR needs inline + eval) via a small VitetransformIndexHtmlplugin. Locksdefault-srcto'self', restrictsimg-srcto YouTube's CDNs,connect-srcto noembed + lrclib,media-srcto self + blob, forbidsobject-src/base-uri.style-srckeeps'unsafe-inline'for React's inline style attributes (never for script).YouTubeLinkCardvalidates the noembed-supplied thumbnail (https+ aytimg/ggphthost) before using it as<img src>; drops it otherwise (card still shows title/author).encodeURIComponentthe id in the noembed request URL (a no-op for a valid 11-char id, but prevents query-param injection from a hypothetical unvalidated id).Why build-only CSP
A
<meta>CSP inindex.htmlapplies in dev too and would break Vite's HMR (inline scripts + eval). Injecting it only forcommand === 'build'keepsnpm run devworking while shipping the policy to production.Verification
Verified against the production build served under
/melody/(matching the real deploy base):script-src 'self')font-src 'self')media-src blob:audio not blockedConfirmed there are no inline
<script>s in the built HTML (both the module bundle andregisterSW.jsare external), soscript-src 'self'is sufficient.🤖 Generated with Claude Code
Generated by Claude Code