Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add support for named appends #283

Merged
merged 2 commits into from Sep 6, 2016
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
55 changes: 55 additions & 0 deletions README.md
Expand Up @@ -94,6 +94,61 @@ use SecureHeaders::Middleware

All headers except for PublicKeyPins have a default value. See the [corresponding classes for their defaults](https://github.com/twitter/secureheaders/tree/master/lib/secure_headers/headers).

## Named Appends

Named Appends are blocks of code that can be reused and composed during requests. e.g. If a certain partial is rendered conditionally, and the csp needs to be adjusted for that partial, you can create a named append for that situation. The value returned by the block will be passed into `append_content_security_policy_directives`. The current request object is passed as an argument to the block for even more flexibility.

```ruby
def show
if include_widget?
@widget = widget.render
use_content_security_policy_named_append(:widget_partial)
end
end


SecureHeaders::Configuration.named_append(:widget_partial) do |request|
if request.controller_instance.current_user.in_test_bucket?
{ child_src: %w(beta.thirdpartyhost.com) }
else
{ child_src: %w(thirdpartyhost.com) }
end
end
```

You can use as many named appends as you would like per request, but be careful because order of inclusion matters. Consider the following:

```ruby
SecureHeader::Configuration.default do |config|
config.csp = { default_src: %w('self')}
end

SecureHeaders::Configuration.named_append(:A) do |request|
{ default_src: %w(myhost.com) }
end

SecureHeaders::Configuration.named_append(:B) do |request|
{ script_src: %w('unsafe-eval') }
end
```

The following code will produce different policies due to the way policies are normalized (e.g. providing a previously undefined directive that inherits from `default-src`, removing host source values when `*` is provided. Removing `'none'` when additional values are present, etc.):

```ruby
def index
use_content_security_policy_named_append(:A)
use_content_security_policy_named_append(:B)
# produces default-src 'self' myhost.com; script-src 'self' myhost.com 'unsafe-eval';
end

def show
use_content_security_policy_named_append(:B)
use_content_security_policy_named_append(:A)
# produces default-src 'self' myhost.com; script-src 'self' 'unsafe-eval';
end
```


## Named overrides

Named overrides serve two purposes:
Expand Down
9 changes: 9 additions & 0 deletions lib/secure_headers.rb
Expand Up @@ -72,6 +72,11 @@ def append_content_security_policy_directives(request, additions)
override_secure_headers_request_config(request, config)
end

def use_content_security_policy_named_append(request, name)
additions = SecureHeaders::Configuration.named_appends(name).call(request)
append_content_security_policy_directives(request, additions)
end

# Public: override X-Frame-Options settings for this request.
#
# value - deny, sameorigin, or allowall
Expand Down Expand Up @@ -267,4 +272,8 @@ def override_content_security_policy_directives(additions)
def override_x_frame_options(value)
SecureHeaders.override_x_frame_options(request, value)
end

def use_content_security_policy_named_append(name)
SecureHeaders.use_content_security_policy_named_append(request, name)
end
end
11 changes: 11 additions & 0 deletions lib/secure_headers/configuration.rb
Expand Up @@ -46,6 +46,17 @@ def get(name = DEFAULT_CONFIG)
@configurations[name]
end

def named_appends(name)
@appends ||= {}
@appends[name]
end

def named_append(name, target = nil, &block)
@appends ||= {}
raise "Provide a configuration block" unless block_given?
@appends[name] = block
end

private

# Private: add a valid configuration to the global set of named configs.
Expand Down
22 changes: 22 additions & 0 deletions spec/lib/secure_headers_spec.rb
Expand Up @@ -151,6 +151,28 @@ module SecureHeaders
expect(hash[CSP::HEADER_NAME]).to eq("default-src 'self'; script-src mycdn.com 'unsafe-inline' anothercdn.com")
end

it "supports named appends" do
Configuration.default do |config|
config.csp = {
default_src: %w('self')
}
end

Configuration.named_append(:moar_default_sources) do |request|
{ default_src: %w(https:)}
end

Configuration.named_append(:how_about_a_script_src_too) do |request|
{ script_src: %w('unsafe-inline')}
end

SecureHeaders.use_content_security_policy_named_append(request, :moar_default_sources)
SecureHeaders.use_content_security_policy_named_append(request, :how_about_a_script_src_too)
hash = SecureHeaders.header_hash_for(request)

expect(hash[CSP::HEADER_NAME]).to eq("default-src 'self' https:; script-src 'self' https: 'unsafe-inline'")
end

it "dups global configuration just once when overriding n times and only calls idempotent_additions? once" do
Configuration.default do |config|
config.csp = {
Expand Down