Skip to content

fix: add Content-Security-Policy header to deployment configs (#230)#259

Merged
Mosas2000 merged 39 commits intomainfrom
fix/csp-deployment-headers
Mar 12, 2026
Merged

fix: add Content-Security-Policy header to deployment configs (#230)#259
Mosas2000 merged 39 commits intomainfrom
fix/csp-deployment-headers

Conversation

@Mosas2000
Copy link
Copy Markdown
Owner

Summary

Adds a comprehensive Content-Security-Policy header to both vercel.json and netlify.toml deployment configurations, closing a security gap where both files had other security headers (X-Frame-Options, X-Content-Type-Options, Referrer-Policy, Permissions-Policy) but no CSP.

Problem

The frontend/public/_headers file already defined a CSP policy, but the Vercel and Netlify deployment configs did not include it. Depending on which deployment platform serves the headers, the CSP may or may not have been enforced. This inconsistency left a window for XSS, clickjacking, and data exfiltration attacks on some deployment targets.

Changes

CSP Directives (11 total, identical across all 3 files)

Directive Value Rationale
default-src 'self' Restrict all resources to same-origin by default
`script-src script-sr Block injected/external scripts (primary XSS defense)
style-src 'self' 'unsafe-inline' Allow Tailwind/inline styles
img-src 'self' data: https: Allow inline SVGs, data URIs, HTTPS images
font-src 'self' data: Allow same-origin fonts and base64 subsets
connect-src 'self' + 5 API origins Whitelist Hiro API (3 origins), CoinGecko, legacy Stacks node
frame-ancestors frame-ancestors frame-ancestors
  • scripts/validate-csp.cjs: Extracts CSP from all 3 config files and verifies they are identical. Provides directive-level diff on mismatch.
  • scripts/validate-csp.test.cjs: 3 tests covering the validator.
  • validate:csp npm script in root package.json.

Files Changed

  • vercel.json - Added CSP header with 11 directives
  • netlify.toml - Added CSP header with 11 directives
  • frontend/public/_headers - Added object-src 'none' and upgrade-insecure-requests (was missing these 2)
  • scripts/validate-csp.cjs - New consistency checker
  • scripts/validate-csp.test.cjs - New tests
  • package.json - Added validate:csp script
  • CHANGELOG.md - Documented changes

Testing

  • 417 frontend tests passing (vitest)
  • 41 chainhook tests passing (node:test)
  • 3 CSP validation tests passing (node:test)
  • node scripts/validate-csp.cjs confirms all 3 configs match with 11 directives

Commit Count

39 commits with incremental, reviewable changes.

Closes #230

The deployment configuration includes several security headers but is
missing a Content-Security-Policy header entirely. Begin building the
CSP by adding the most restrictive baseline directive that limits all
resource loading to the same origin by default.

This mirrors the existing policy already defined in the static
frontend/public/_headers file, ensuring Vercel deployments enforce
the same restrictions.
Restrict JavaScript execution to same-origin scripts only. This
prevents inline script injection and blocks any externally hosted
scripts from running, which is the primary defense against reflected
and stored XSS attacks.

The application bundles all JavaScript through Vite, so no external
script sources are required.
Allow stylesheets from the same origin plus inline styles. The
unsafe-inline exception is necessary because Tailwind CSS and several
React component libraries inject styles at runtime via style
attributes and dynamically created style elements.

Without this exception the application layout would break on first
load. A future improvement could migrate to nonce-based inline styles
to remove the unsafe-inline allowance.
Allow images from the same origin, data: URIs for inline SVG and
base64-encoded icons, and any HTTPS source. The broad https: allowance
covers user-generated avatar URLs and potential future CDN usage
without requiring a CSP update for each new image host.

Data URIs are needed for some SVG icons rendered inline by the
component library.
Restrict font loading to same-origin files and data: URIs. Data URIs
are required for base64-encoded font subsets that some icon libraries
embed directly in CSS to avoid extra network requests.

No external font CDN is used by the application, so there is no need
to whitelist any third-party origins for fonts.
Begin the connect-src directive with same-origin only. This controls
which URLs the application can contact via fetch, XMLHttpRequest,
WebSocket, and EventSource. The baseline is restrictive and will be
expanded in subsequent commits to whitelist specific API endpoints
that the frontend depends on.
The Hiro API at https://api.hiro.so is the primary Stacks blockchain
API used by the frontend for transaction broadcasting, contract calls,
and balance lookups. This is the base URL used by the Stacks.js
library when connecting to mainnet.

Without this entry, all blockchain interactions would be blocked by
the browser CSP enforcement.
Add the explicit mainnet Stacks API endpoint. Some Stacks.js
configurations and direct API calls use the network-specific subdomain
rather than the generic api.hiro.so base URL.

Both origins are required to ensure all mainnet API calls succeed
regardless of which URL pattern the client library resolves to.
Include the testnet API endpoint for development and staging builds
that may target the Stacks testnet. While the production VITE_NETWORK
is set to mainnet, developers running local builds against testnet or
staging environments deployed on Vercel need this origin to be
permitted.

This ensures the CSP does not silently break testnet workflows.
The useStxPrice hook fetches the current STX/USD exchange rate from
the CoinGecko API to display fiat-equivalent tip amounts. This is
the only non-Stacks external API the frontend connects to.

The endpoint used is /api/v3/simple/price with ids=blockstack and
vs_currencies=usd. Without this CSP entry the price feed would fail
silently and users would not see USD equivalents.
Add the legacy Stacks node API endpoint that some older library
versions and direct integrations still reference. This origin serves
the same role as api.mainnet.hiro.so but under the original Hiro
infrastructure domain.

Keeping both origins whitelisted ensures backward compatibility
during the ongoing API migration and prevents breakage if any
dependency still resolves to this endpoint.
Prevent the application from being embedded in any frame, iframe, or
object element on any origin. This is the CSP equivalent of the
X-Frame-Options: DENY header already present in the configuration,
but frame-ancestors is the modern replacement that is respected by
all current browsers.

Both headers are kept for defense in depth since older browsers may
only support X-Frame-Options.
Restrict the base element to same-origin URLs only. This prevents an
attacker who can inject HTML from changing the base URL of the page,
which would cause all relative URLs (scripts, stylesheets, links) to
resolve against a malicious origin.

The application does not use a base element at all, but locking this
directive down prevents it from being exploited if one were ever
injected.
Restrict form submissions to same-origin targets only. This prevents
a CSP bypass where an attacker injects a form element that submits
user data to an external server. Although the application is a
single-page app and does not use traditional form submissions, this
directive closes the attack surface as a defense-in-depth measure.

The Vercel CSP header now matches all directives present in the
existing frontend/public/_headers file.
Apply the same Content-Security-Policy baseline to the Netlify
deployment configuration. The netlify.toml file has the same set of
security headers as vercel.json but was also missing a CSP header.

Start with the most restrictive fallback directive that limits all
resource types to same-origin by default.
Restrict JavaScript execution to same-origin scripts only on Netlify
deployments. All application scripts are bundled by Vite and served
from the same origin, so no external script sources are needed.

This is the most important CSP directive for XSS prevention as it
blocks execution of any injected script elements.
Allow same-origin stylesheets and inline styles on Netlify. The
unsafe-inline keyword is required because Tailwind CSS generates
utility classes that are injected at runtime, and several UI
components apply dynamic inline styles.

This matches the style-src policy already in vercel.json and the
static _headers file.
Allow images from same origin, data URIs, and any HTTPS source on
Netlify. Data URIs are needed for inline SVG icons and base64-encoded
images used by the UI component library.

The broad https: scheme covers user avatars and potential CDN images
without requiring individual origin whitelisting for each image host.
Restrict font loading to same-origin and data URIs on Netlify. No
external font CDNs are used by the application. Data URIs cover
base64-embedded font subsets that some icon libraries include
directly in CSS bundles.

This matches the font-src policy already applied to vercel.json.
Begin the connect-src directive for Netlify with same-origin only.
This governs all fetch, XMLHttpRequest, and WebSocket connections.
External API origins will be added in subsequent commits to keep each
allowance explicit and auditable.

The incremental approach makes it clear which origins the application
actually requires for network access.
Allow connections to the primary Hiro Stacks API from Netlify
deployments. This is the base URL used by the Stacks.js connect
library for all blockchain interactions including transaction
broadcasting, read-only contract calls, and balance queries.

This is the same origin already whitelisted in the Vercel CSP.
Add the explicit mainnet Stacks API subdomain to the Netlify CSP.
Some Stacks.js configurations resolve to this network-specific
subdomain rather than the generic api.hiro.so URL.

Both origins must be permitted to ensure all mainnet API requests
succeed regardless of the URL pattern used by the client library.
Include the testnet API endpoint for Netlify deployments. Preview
deployments and branch deploys on Netlify may target the Stacks
testnet for QA purposes, and this origin must be allowed for those
builds to function correctly.

This completes the Hiro API origin set: base, mainnet, and testnet.
Allow the CoinGecko price API on Netlify deployments. The useStxPrice
hook fetches the STX/USD exchange rate from this endpoint to show
fiat-equivalent tip values in the UI.

This is the only non-blockchain external API the frontend depends on.
Without it the price feed would fail silently on Netlify-hosted
builds.
Add the legacy Stacks node API origin to the Netlify CSP. This
endpoint predates the Hiro API rebrand and is still referenced by
some older Stacks.js library versions and direct integrations.

Including it maintains backward compatibility during the ongoing
migration to the newer api.hiro.so domain structure.
Block all framing of the application on Netlify deployments. This is
the modern CSP replacement for X-Frame-Options: DENY which is already
set in the same header block.

Both mechanisms are retained for defense in depth. Frame-ancestors is
supported by all modern browsers while X-Frame-Options provides
fallback coverage for older user agents.
Restrict the HTML base element to same-origin URLs on Netlify. This
prevents an attacker who achieves HTML injection from hijacking the
document base URL to redirect relative resource references to a
malicious server.

The application does not use a base element, but this directive
proactively blocks the attack vector.
Restrict form submissions to same-origin targets on Netlify. This
closes a potential data exfiltration vector where an attacker could
inject a form element that posts user input to an external server.

The Netlify CSP header now contains all directives matching both the
Vercel deployment configuration and the static _headers file. All
three deployment targets enforce an identical Content-Security-Policy.
Create scripts/validate-csp.cjs that extracts the Content-Security-
Policy value from all three deployment configuration files (_headers,
vercel.json, netlify.toml) and verifies they are identical.

The script parses each config format independently without third-party
dependencies and provides directive-level diff output when a mismatch
is detected. This can be integrated into CI to prevent CSP drift
across deployment targets.
Verify that the validation script correctly identifies matching CSP
policies across all three deployment configs. Tests check the exit
code, the directive count output, and the source listing.

Uses node:test to stay consistent with the chainhook test approach
and avoid additional test framework dependencies.
Register the CSP validation script as an npm run target so it can be
invoked via 'npm run validate:csp' from the project root. This makes
it easy to integrate into CI pipelines and pre-commit hooks.

The script exits with code 1 if any CSP mismatch is detected between
the three deployment configuration files.
Block all plugin content including Flash, Java applets, and embedded
objects. The application has no use for browser plugins and this
directive is recommended by the OWASP CSP guidelines as a baseline
hardening measure.

While default-src 'self' would fall back for object-src, explicitly
setting it to 'none' makes the intent clear and prevents any
accidental relaxation if default-src were ever broadened.
Sync the new object-src directive to the Vercel deployment config.
This blocks all plugin-based content on Vercel-hosted builds,
matching the updated _headers policy.

This is a strict deny rather than falling back to default-src, making
the policy self-documenting and resilient to future changes.
Sync the object-src directive to the Netlify deployment config,
completing the three-way consistency for this new directive.

All three CSP sources now define 10 directives with identical values.
Instruct browsers to automatically upgrade any HTTP requests to HTTPS
before they are sent. This is a transport-level protection that
ensures no mixed-content warnings or accidental plaintext requests
can occur, even if a resource URL is accidentally specified with
http:// in the source code.

Both Vercel and Netlify serve over HTTPS, so all requests should
already be secure, but this directive provides an additional safety
net at the browser level.
Sync the transport upgrade directive to Vercel. Browsers will
transparently rewrite any http:// sub-resource requests to https://
before the connection is established.

This is especially important for the connect-src origins where an
accidental http:// prefix on an API URL would otherwise leak request
data in plaintext.
Complete the three-way sync for the upgrade-insecure-requests
directive. All deployment configs now instruct browsers to
automatically upgrade HTTP requests to HTTPS.

The CSP now contains 11 directives across all three configuration
files.
Document the security improvement of adding Content-Security-Policy
headers to both vercel.json and netlify.toml. List all 11 CSP
directives, the new object-src and upgrade-insecure-requests
additions to the static _headers file, the validate-csp.cjs
consistency checker, and the validate:csp npm script.

Entries are placed in the [Unreleased] section under Security and
Added categories following the Keep a Changelog format.
The directive count increased from 9 to 11 after adding object-src
and upgrade-insecure-requests to all three CSP sources. Update the
test assertion to match the current policy.
@Mosas2000 Mosas2000 merged commit c6346fb into main Mar 12, 2026
2 of 6 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.

🛡️ MEDIUM: Add Content-Security-Policy header to deployment configs

1 participant