CSP Maker

CSP Maker is a micro gem in Ruby for defining a Content-Security-Policy, extracted from Rails so it can be used standalone or in Rack apps.

The code is extracted from ActionDispatch::ContentSecurityPolicy in Action Pack, with some minor modifications. The DSL as described in Rails docs should be usable 1:1 here. There is also an optional Rack middleware updated to not rely on Rails internals.

Installation

Add the gem to your app's Gemfile:

gem 'csp_maker', '~> 0.1'

Then, require it in your server code:

require 'csp_maker'

CSP Maker supports Ruby 3.0 and newer.

Basic usage

The simplest way is through the CSPMaker.build_policy method:

headers['Content-Security-Policy'] = CSPMaker.build_policy do |p|
  p.default_src :none
  p.script_src :self, 'https://github.com', 'https://example.com'
  p.style_src :self
  p.font_src :self
  p.img_src :self, :https, :data
  p.connect_src :self, 'https://api.bsky.app'
  p.base_uri :none
  p.object_src :none
end

This creates a ContentSecurityPolicy definition object and then immediately encodes it into the final string, which you can assign to the target header.

Alternatively, you can also pass a block without parameters and call the DSL methods on an implicit self like this:

headers['Content-Security-Policy'] = CSPMaker.build_policy do
  default_src :none
  script_src :self, 'https://github.com', 'https://example.com'
  style_src :self
  font_src :self
  img_src :self, :https, :data
  connect_src :self, 'https://api.bsky.app'
  base_uri :none
  object_src :none
end

Note, in this case (block without parameters) the block is run through instance_eval in the context of the policy object, so helper methods and instance vars from outside the block won't be accessible inside; if you want to use those, use the version with a parameter, or call the ContentSecurityPolicy constructor directly.

If you want to use nonces, you can generate a nonce yourself and pass it to the DSL using a new nonce() DSL method like this:

asset_nonce = ...

headers['Content-Security-Policy'] = CSPMaker.build_policy do
  default_src :none
  script_src :self, nonce(asset_nonce)
end

A second approach is to use CSPMaker.define_policy to create a policy definition once, and then reuse it on each request:

CSP = CSPMaker.define_policy do
  default_src :none
  script_src :self, 'https://github.com', 'https://example.com'
  style_src :self
  ...
end

You encode the policy into the header string by calling #build(context, nonce, nonce_directives). The parameters (all optional) are:

  • context – you can pass a Proc instead of a String/Symbol to a DSL directive, and that Proc will be called with this context object as self (it can be e.g. some kind of controller or request object)
  • nonce – a nonce to be added to the designated directives
  • nonce_directives – array of directives to which nonces should be added; if nil, the list is read from a global setting CSPMaker.nonce_directives, or the default ['script-src', 'style-src']

So it can look like this:

asset_nonce = ...

headers['Content-Security-Policy'] = CSP.build(nil, asset_nonce, ['script-src'])

Rack middleware

A third way is to use the Rack middleware. The middleware class is also taken from the Rails code, but simplified to not rely on Rails internals.

Require the csp_maker/rack file instead of csp_maker:

require 'csp_maker/rack'

Define a policy, and pass it to the CSPMaker::Middleware when installing it into the Rack stack:

policy = CSPMaker.define_policy {
  ...
}

use CSPMaker::Middleware, policy

The middleware will:

  • automatically generate a nonce, append it to the directives and store it in the Rack env, if a nonce generator is configured (see below)
  • store the generated nonce in the env under csp_maker.nonce (CSPMaker::Middleware::NONCE_ENV_KEY)
  • create a Rack::Request object and use it as the block context for any Procs in the DSL
  • encode the policy into a result string
  • assign it to the content-security-policy header automatically

Using nonces

When using the Rack middleware, if you want it to append nonces, you need to assign a nonce generator Proc to CSPMaker.nonce_generator (equivalent of config.content_security_policy_nonce_generator in Rails), or pass it as a nonce_generator: option to the Middleware initializer. You can use CSPMaker.default_generator, which calls SecureRandom.base64(16):

CSPMaker.nonce_generator = CSPMaker.default_generator

# or:

CSPMaker.nonce_generator = -> { ... }

Use CSPMaker.nonce_directives (equivalent of config.content_security_policy_nonce_directives in Rails) or nonce_directives: option in Middleware to configure which directives should have a nonce added to them (default is ['script-src', 'style-src']):

CSPMaker.nonce_directives = ['script-src']

# or:

use CSPMaker::Middleware, policy, nonce_generator: -> { ... }, nonce_directives: ['style-src']

The generated nonce is stored in the env hash under csp_maker.nonce (CSPMaker::Middleware::NONCE_ENV_KEY). You will need it for the view template code to add it to the inline script / style tags, so you could add a helper like:

def asset_nonce
  request.env[CSPMaker::Middleware::NONCE_ENV_KEY]
end

When using build_policy or define_policy standalone without a middleware, generating and storing the nonce is up to you – and you either pass it to nonce(...) inside a build_policy block, or as the second argument to #build on a CSPMaker::ContentSecurityPolicy object returned from define_policy. However, you can also assign CSPMaker.nonce_generator and call CSPMaker.make_nonce to run it if you want:

CSPMaker.nonce_generator = CSPMaker.default_generator

policy = CSPMaker.define_policy { ... }
nonce = CSPMaker.make_nonce

headers['content-security-policy'] = policy.build(nil, nonce)

Other options

You can also set CSPMaker.report_only (equivalent of config.content_security_policy_report_only in Rails) or report_only: option to Middleware to assign the policy to the content-security-policy-report-only header instead of content-security-policy; this makes the browser only report errors to the URL configured via report_uri in the DSL, but not actually enforce the policy in the web app (i.e. not block any scripts & styles from loading).

Full DSL API

Directives:

  • directives that accept an array of sources: base_uri, child_src, connect_src, default_src, font_src, form_action, frame_ancestors, frame_src, img_src, manifest_src, media_src, object_src, prefetch_src, require_trusted_types_for, script_src, script_src_attr, script_src_elem, style_src, style_src_attr, style_src_elem, trusted_types, worker_src
  • block_all_mixed_content(enabled = true)
  • plugin_types(*types)
  • report_uri(uri)
  • require_sri_for(*types)
  • sandbox(enabled = true) or sandbox(*values)
  • upgrade_insecure_requests(enabled = true)

Source lists:

  • special values: :allow_duplicates, :none, :report_sample, :script, :self, :strict_dynamic, :unsafe_eval, :unsafe_hashes, :unsafe_inline, :wasm_unsafe_eval
  • protocols: :http, :https, :data, :mediastream, :blob, :filesystem, :ws, :wss
  • content hashes: "sha256-...", "sha384-...", "sha512-..."
  • nonce(nonce_value)

Credits

The original content_security_policy.rb was added to Rails ActionPack code by Andrew White in 2017, and had some updates since then by others.

Modifications for the purposes of this gem are © 2026 Kuba Suder (@mackuba.eu).

The code is available under the terms of the MIT license.

Bug reports and pull requests are welcome :)