Credence Frontend applies a Content-Security-Policy (CSP) header as a defence-in-depth layer against cross-site scripting (XSS) and data injection attacks. The policy is enforced during development via the Vite dev server and must be replicated in the production hosting layer (see Production Deployment).
Content-Security-Policy: default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; font-src 'self'; connect-src 'self' ws://localhost:*; base-uri 'self'; form-action 'self'; frame-ancestors 'none'| Directive | Value | Rationale |
|---|---|---|
default-src |
'self' |
Baseline — all resources restricted to the same origin unless overridden. |
script-src |
'self' |
Scripts from own origin only. Vite bundles all JS into hashed files; no inline scripts are used in production. |
style-src |
'self' 'unsafe-inline' |
Required by Vite's CSS-module injection and React's inline styles. 'unsafe-inline' is the minimum concession needed for SPA rendering. |
img-src |
'self' data: |
Same-origin images plus data URIs (used for inline icons / placeholders). |
font-src |
'self' |
Font assets served from the same origin. |
connect-src |
'self' ws://localhost:* |
API calls to the same origin, plus WebSocket connections for Vite HMR in development. |
base-uri |
'self' |
Prevents attackers from injecting <base> tags to hijack relative URLs. |
form-action |
'self' |
Restricts form submissions to the same origin. |
frame-ancestors |
'none' |
Precludes clickjacking by forbidding the app from being embedded in <frame>, <iframe>, or <object>. |
A lax or absent CSP allows an attacker who achieves script injection (e.g. through a compromised dependency, unescaped user input, or a DOM-based XSS gadget) to:
- Exfiltrate authentication tokens, wallet addresses, or session data to an attacker-controlled endpoint.
- Modify the DOM to phish user secrets (fake wallet-connection prompts, credential harvesters).
- Load and execute arbitrary third-party scripts.
The tightened script-src 'self' directive raises the bar from "any script runs" to "only scripts signed by the application's own build artefact run." This is a defence-in-depth measure — it does not replace input sanitisation or output encoding, but it contains the blast radius of a bypass in those layers.
A CSP that is too strict will break the application:
- Do not remove
'unsafe-inline'fromstyle-src— Vite and React inject inline<style>elements for CSS modules and component styles. Without it all styling collapses. - Do not add
'unsafe-inline'toscript-src— this would defeat the purpose of the policy. Vite does not emit inline scripts in production builds. - Do not remove
ws://localhost:*fromconnect-srcwithout also adjusting the dev-server port — Vite HMR uses a WebSocket connection in development.
In production, the ws://localhost:* entry is harmless because the origin restriction still applies (the browser will only connect WebSockets to localhost).
The CSP string is defined in src/config/security.ts and imported by vite.config.ts. Always modify the policy in security.ts — it is the single source of truth and is exercised by the unit test at src/config/security.test.ts.
The CSP header set by vite.config.ts only applies to the Vite dev server (npm run dev). For production, configure your web server or hosting platform to send the same Content-Security-Policy header with the static build output (dist/).
For backend cookie-signing secrets (session and CSRF), see the Cookie-Secret Rotation Runbook.
add_header Content-Security-Policy "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; font-src 'self'; connect-src 'self'; base-uri 'self'; form-action 'self'; frame-ancestors 'none'";Note: ws://localhost:* is omitted from the production configuration because Vite HMR is not present in production builds.
A negative test in src/config/security.test.ts verifies that:
script-src 'self'is present.'unsafe-eval'and'unsafe-inline'are not present inscript-src.style-src 'self' 'unsafe-inline'is present.- The policy does not contain a dangling or malformed directive.
The test would fail without the CSP configuration in place.
If a <script src> or <link href> loads a resource from a CDN or any cross-origin host, a compromised CDN can silently swap that resource for malicious code. The browser executes or applies whatever bytes it receives with no indication that they differ from the original. For a wallet-adjacent application this means an attacker could:
- Exfiltrate private keys, seed phrases, or session tokens entered by the user.
- Replace the wallet-connect UI with a phishing variant that harvests credentials.
- Inject transaction-manipulation logic that operates silently in the background.
SRI (integrity="sha384-…" crossorigin="anonymous") instructs the browser to hash the received bytes and abort loading if the digest does not match the expected value. The crossorigin attribute is required alongside integrity because it forces the browser to make a CORS request, enabling inspection of the response bytes.
Every <script src> or <link href> that loads from a cross-origin URL must carry both integrity and crossorigin attributes. Same-origin (relative) assets do not require SRI because they are served directly by the application's own infrastructure.
<!-- ✅ Correct — CDN asset with SRI hash -->
<script
src="https://cdn.jsdelivr.net/npm/some-lib@1.0/dist/lib.min.js"
integrity="sha384-<hash>"
crossorigin="anonymous"
></script>
<!-- ❌ Wrong — CDN asset without SRI -->
<script src="https://cdn.jsdelivr.net/npm/some-lib@1.0/dist/lib.min.js"></script># For a URL:
curl -sL https://cdn.jsdelivr.net/npm/some-lib@1.0/dist/lib.min.js \
| openssl dgst -sha384 -binary | openssl base64 -A | xargs -I{} echo "sha384-{}"
# Or use https://www.srihash.orgsrc/lib/sriCheck.ts provides a checkSri(htmlSource) function that parses any HTML string and returns a typed SriResult:
import { checkSri } from '../lib/sriCheck'
const result = checkSri(htmlSource)
if (!result.ok) {
// result.violations: SriViolation[]
// Each violation has: kind ('missing-integrity' | 'missing-crossorigin' | 'missing-both')
// asset.tag, asset.url, message
}The unit tests in src/lib/sriCheck.test.ts include:
- Negative tests — verify that CDN assets without
integrity/crossoriginare flagged (these tests demonstrate the exact vulnerability). - Positive tests — verify that compliant assets pass.
- Regression test — reads the actual
index.htmlat the project root and asserts it contains no violations. This test will fail in CI if a future PR adds a CDN tag without an SRI hash.