fix: add Content-Security-Policy header to deployment configs (#230)#259
Merged
fix: add Content-Security-Policy header to deployment configs (#230)#259
Conversation
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.
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.
Summary
Adds a comprehensive Content-Security-Policy header to both
vercel.jsonandnetlify.tomldeployment 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/_headersfile 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)
default-src'self'script-srstyle-src'self' 'unsafe-inline'img-src'self' data: https:font-src'self' data:connect-src'self'+ 5 API originsframe-ancestorsframe-ancestorsframe-ancestorsscripts/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:cspnpm script in rootpackage.json.Files Changed
vercel.json- Added CSP header with 11 directivesnetlify.toml- Added CSP header with 11 directivesfrontend/public/_headers- Addedobject-src 'none'andupgrade-insecure-requests(was missing these 2)scripts/validate-csp.cjs- New consistency checkerscripts/validate-csp.test.cjs- New testspackage.json- Addedvalidate:cspscriptCHANGELOG.md- Documented changesTesting
node scripts/validate-csp.cjsconfirms all 3 configs match with 11 directivesCommit Count
39 commits with incremental, reviewable changes.
Closes #230