Skip to content

feat: add CSP and some security headers to HTML pages#2075

Open
serhalp wants to merge 10 commits intomainfrom
serhalp/security-audit
Open

feat: add CSP and some security headers to HTML pages#2075
serhalp wants to merge 10 commits intomainfrom
serhalp/security-audit

Conversation

@serhalp
Copy link
Member

@serhalp serhalp commented Mar 14, 2026

🔗 Linked issue

N/A but discussed on Discord here: https://discord.com/channels/1464542801676206113/1465055739293991033/1481201825242681394

🧭 Context

We're missing a few basic security protections such as a Content Security Policy on HTML responses, X-Frame-Options, etc.

You can see the result of a basic scan here: https://developer.mozilla.org/en-US/observatory/analyze?host=main.npmx.dev.

📚 Description

Add a CSP to the HTML layout and a few security headers (X-Content-Type-Options, X-Frame-Options, Referrer-Policy) on server responses.

Adding the CSP to the HTML layout directly with a <meta> tag is way simpler than trying to configure a response header conditionally only on html responses while excluding API responses, static assets, special internal paths, and whatever else, and then setting the CSP on pre-rendered pages also by either configuring static headers for those or using the <meta> approach only for those 😓.

The CSP img-src directive imports TRUSTED_IMAGE_DOMAINS from the image proxy module so the two stay in sync automatically, and similarly for supported git hosts.

Warning

It's unlikely I found all the hosts we need to allowlist. Please help me find any missing ones!

This PR also adds an e2e test that should fail on future PRs if a request is blocked by a CSP violation in the app's main happy path.

Add a global Nitro middleware that sets CSP and security headers (X-Content-Type-Options,
X-Frame-Options, Referrer-Policy) on page responses. API routes and internal paths (/_nuxt/, /_v/,
etc.) are skipped.

The CSP img-src directive imports TRUSTED_IMAGE_DOMAINS from the image proxy module so the two stay
in sync automatically.
@vercel
Copy link

vercel bot commented Mar 14, 2026

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

Project Deployment Actions Updated (UTC)
npmx.dev Ready Ready Preview, Comment Mar 22, 2026 3:32am
2 Skipped Deployments
Project Deployment Actions Updated (UTC)
docs.npmx.dev Ignored Ignored Preview Mar 22, 2026 3:32am
npmx-lunaria Ignored Ignored Mar 22, 2026 3:32am

Request Review

@codecov
Copy link

codecov bot commented Mar 14, 2026

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ All tests successful. No failed tests found.

📢 Thoughts on this report? Let us know!

@serhalp serhalp marked this pull request as ready for review March 14, 2026 22:03
@serhalp serhalp changed the title feat: add CSP and other security headers to HTML responses feat: add CSP and some security headers to HTML pages Mar 14, 2026
@serhalp serhalp requested a review from danielroe March 14, 2026 22:04
@coderabbitai
Copy link
Contributor

coderabbitai bot commented Mar 14, 2026

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 3ac98976-6218-45e7-bac9-2ddca9407577

📥 Commits

Reviewing files that changed from the base of the PR and between 59af702 and de9d588.

📒 Files selected for processing (1)
  • modules/security-headers.ts

📝 Walkthrough

Walkthrough

Replaces hardcoded git provider API URLs with a new centralized mapping (GIT_PROVIDER_API_ORIGINS) and adds ALL_KNOWN_GIT_API_ORIGINS. Exports TRUSTED_IMAGE_DOMAINS. Adds a Nuxt module modules/security-headers.ts that builds and injects a Content-Security-Policy meta tag into HTML head and merges global route rules to set X-Content-Type-Options, X-Frame-Options and Referrer-Policy. Adds end-to-end tests that assert CSP/meta presence on HTML pages, absence of CSP on a specific API route, and collects CSP violation console messages via a new test fixture.

Possibly related PRs

Suggested reviewers

  • danielroe
  • ghostdevv
  • 43081j
🚥 Pre-merge checks | ✅ 1
✅ Passed checks (1 passed)
Check name Status Explanation
Description check ✅ Passed The pull request description is directly related to the changeset, detailing the addition of security headers and CSP to HTML pages with context on why this approach was chosen.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch serhalp/security-audit

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (2)
modules/security-headers.ts (1)

35-37: Consider scoping localhost connect-src to dev or a feature flag.

Allowing http://127.0.0.1:* on all builds broadens the outbound policy surface. If this is only needed for local CLI integration in specific scenarios, gate it to reduce exposure.

Possible narrowing diff
+    const localhostConnectSrc = nuxt.options.dev ? ['http://127.0.0.1:*'] : []
+
     const connectSrc = [
       "'self'",
       'https://*.algolia.net',
       'https://registry.npmjs.org',
       'https://api.npmjs.org',
       'https://npm.antfu.dev',
       ...ALL_KNOWN_GIT_API_ORIGINS,
       // Local CLI connector (npmx CLI communicates via localhost)
-      'http://127.0.0.1:*',
+      ...localhostConnectSrc,
     ].join(' ')
server/utils/image-proxy.ts (1)

32-32: Harden TRUSTED_IMAGE_DOMAINS as read-only constant to prevent accidental mutation.

TRUSTED_IMAGE_DOMAINS is a security-sensitive constant shared across modules. Although current usage is read-only, exporting as a mutable array risks future code accidentally widening the trust boundary via push(), splice(), or similar operations. Apply as const or Object.freeze() to enforce immutability.

Suggested hardening
-export const TRUSTED_IMAGE_DOMAINS = [
+export const TRUSTED_IMAGE_DOMAINS = [
   // First-party
   'npmx.dev',
   ...
-]
+] as const

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: bf5a1962-4e1b-45ce-82ba-9996da979595

📥 Commits

Reviewing files that changed from the base of the PR and between a170292 and 49438d8.

📒 Files selected for processing (6)
  • app/composables/useRepoMeta.ts
  • modules/security-headers.ts
  • server/utils/image-proxy.ts
  • shared/utils/git-providers.ts
  • test/e2e/security-headers.spec.ts
  • test/e2e/test-utils.ts

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.

2 participants