Skip to main content

How CSP Nonces Work With Turbo in a Rails App

Learn how Inpages.me keeps Content Security Policy nonces stable across Turbo visits, constrains trusted sources, and treats CSP as defense in depth today.

In a browser-based encryption product, a Content Security Policy is not a claim that XSS is impossible. It is one layer that narrows where the browser may load code, submit forms, create frames, or open workers. The hard part is keeping that layer compatible with Turbo navigation and the few integrations the app deliberately uses.

Why CSP matters in a private web app

When protected writing is open in a browser, unexpected script execution has a larger consequence than a cosmetic defect. Inpages.me uses browser-held keys while a vault is unlocked, so the application treats script sources, frames, form destinations, and workers as explicit policy decisions.

OWASP’s CSP guidance describes CSP as defense in depth. That distinction matters: secure rendering, input handling, dependency review, and a threat model still do the primary work.

Start from a restrictive default

The policy begins with default-src 'self', rejects plugin content with object-src 'none', and prevents other sites from embedding the application with frame-ancestors 'none'. Individual directives then name the capabilities the app actually needs.

default-src 'self'
base-uri 'self'
object-src 'none'
frame-ancestors 'none'
script-src 'self' approved-script-hosts nonce
worker-src 'self' blob:

That separation is deliberate. A policy should make a new remote dependency visible in code review instead of allowing every future source by default.

Keep the nonce usable across Turbo visits

Rails can attach a nonce to approved inline scripts, including structured-data blocks. Turbo preserves the current document policy across visits, so generating a disconnected nonce on each response can make a legitimate subsequent visit fail.

Inpages.me derives a stable nonce for the browser session using an HMAC of the session identifier. It does not expose that identifier in the response. The result lets server-rendered nonce-bearing scripts continue to work through Turbo navigation while retaining the policy boundary.

Name external sources by capability

The application names external hosts only where a feature requires them: analytics and map requests in connect-src, approved script providers in script-src, Google sign-in in form-action, and font providers in their relevant directives. Image and media directives permit browser-generated blob: URLs because locally decrypted media is rendered in memory.

Allowing blob: is not permission to load arbitrary remote scripts. The directive that permits an image or worker is different from the directive that permits executable code.

CSP does not make an unlocked browser safe by itself

A permissive source, a compromised trusted dependency, an unsafe rendering path, a malicious extension, or a compromised device can still undermine privacy. The CSP therefore sits beside—not instead of—the limits documented in our end-to-end encryption threat model.

The policy is also not a reason to claim that browser-held keys are protected from all same-origin script. The browser encryption architecture keeps that trade-off explicit.

Review a policy change like an API change

Before adding a source, identify the feature, directive, exact host, data it can receive, and failure mode. Test a page with the feature, a Turbo navigation away and back, and the locked and unlocked vault states. Remove an entry when the integration is removed.

For the adjacent content boundary, see why rich text is sanitized before client-side encryption. Both practices reduce the amount of trust a later rendering step must carry.