Skip to content

Commit

Permalink
Merge branch 'master' into dynamic-csp-config-abstraction
Browse files Browse the repository at this point in the history
  • Loading branch information
oreoshake committed Sep 9, 2016
2 parents 3225ed6 + f083d8c commit 9a3c729
Show file tree
Hide file tree
Showing 7 changed files with 218 additions and 15 deletions.
61 changes: 61 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,64 @@
## 3.4.1 Named Appends

### Small bugfix

If your CSP did not define a script/style-src and you tried to use a script/style nonce, the nonce would be added to the page but it would not be added to the CSP. A workaround is to define a script/style src but now it should add the missing directive (and populate it with the default-src).

### 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?
SecureHeaders.override_x_frame_options(request, "DENY")
{ 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
```

## 3.4.0 the frame-src/child-src transition for Firefox.

Handle the `child-src`/`frame-src` transition semi-intelligently across versions. I think the code best descibes the behavior here:
Expand Down
58 changes: 57 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# Secure Headers [![Build Status](https://travis-ci.org/twitter/secureheaders.png?branch=master)](http://travis-ci.org/twitter/secureheaders) [![Code Climate](https://codeclimate.com/github/twitter/secureheaders.png)](https://codeclimate.com/github/twitter/secureheaders) [![Coverage Status](https://coveralls.io/repos/twitter/secureheaders/badge.png)](https://coveralls.io/r/twitter/secureheaders)
# Secure Headers [![Build Status](https://travis-ci.org/twitter/secureheaders.svg?branch=master)](http://travis-ci.org/twitter/secureheaders) [![Code Climate](https://codeclimate.com/github/twitter/secureheaders.svg)](https://codeclimate.com/github/twitter/secureheaders) [![Coverage Status](https://coveralls.io/repos/twitter/secureheaders/badge.svg)](https://coveralls.io/r/twitter/secureheaders)


**The 3.x branch was recently merged**. See the [upgrading to 3.x doc](upgrading-to-3-0.md) for instructions on how to upgrade including the differences and benefits of using the 3.x branch.
Expand Down Expand Up @@ -123,6 +123,62 @@ end

If **

## 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|
SecureHeaders.override_x_frame_options(request, "DENY")
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
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,11 @@ def append_content_security_policy_directives(request, additions, target = nil)
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 @@ -333,4 +338,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
Original file line number Diff line number Diff line change
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
41 changes: 29 additions & 12 deletions lib/secure_headers/headers/policy_management.rb
Original file line number Diff line number Diff line change
Expand Up @@ -50,16 +50,6 @@ def self.included(base)
FRAME_ANCESTORS = :frame_ancestors
PLUGIN_TYPES = :plugin_types

# These are directives that do not inherit the default-src value. This is
# useful when calling #combine_policies.
NON_FETCH_SOURCES = [
BASE_URI,
FORM_ACTION,
FRAME_ANCESTORS,
PLUGIN_TYPES,
REPORT_URI
]

DIRECTIVES_2_0 = [
DIRECTIVES_1_0,
BASE_URI,
Expand Down Expand Up @@ -124,6 +114,18 @@ def self.included(base)
# everything else is in between.
BODY_DIRECTIVES = ALL_DIRECTIVES - [DEFAULT_SRC, REPORT_URI]

# These are directives that do not inherit the default-src value. This is
# useful when calling #combine_policies.
NON_FETCH_SOURCES = [
BASE_URI,
FORM_ACTION,
FRAME_ANCESTORS,
PLUGIN_TYPES,
REPORT_URI
]

FETCH_SOURCES = ALL_DIRECTIVES - NON_FETCH_SOURCES

VARIATIONS = {
"Chrome" => CHROME_DIRECTIVES,
"Opera" => CHROME_DIRECTIVES,
Expand Down Expand Up @@ -260,8 +262,23 @@ def merge_policy_additions(original, additions)
def populate_fetch_source_with_default!(original, additions)
# in case we would be appending to an empty directive, fill it with the default-src value
additions.keys.each do |directive|
unless original[directive] || !source_list?(directive) || NON_FETCH_SOURCES.include?(directive)
original[directive] = original[:default_src]
if !original[directive] && ((source_list?(directive) && FETCH_SOURCES.include?(directive)) || nonce_added?(original, additions))
if nonce_added?(original, additions)
inferred_directive = directive.to_s.gsub(/_nonce/, "_src").to_sym
unless original[inferred_directive] || NON_FETCH_SOURCES.include?(inferred_directive)
original[inferred_directive] = original[:default_src]
end
else
original[directive] = original[:default_src]
end
end
end
end

def nonce_added?(original, additions)
[:script_nonce, :style_nonce].each do |nonce|
if additions[nonce] && !original[nonce]
return true
end
end
end
Expand Down
2 changes: 1 addition & 1 deletion secure_headers.gemspec
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# -*- encoding: utf-8 -*-
Gem::Specification.new do |gem|
gem.name = "secure_headers"
gem.version = "3.4.0"
gem.version = "3.4.1"
gem.authors = ["Neil Matatall"]
gem.email = ["[email protected]"]
gem.description = 'Security related headers all in one gem.'
Expand Down
51 changes: 50 additions & 1 deletion spec/lib/secure_headers_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,10 @@ module SecureHeaders
end

context "content security policy" do
let(:chrome_request) {
Rack::Request.new(request.env.merge("HTTP_USER_AGENT" => USER_AGENTS[:chrome]))
}

it "appends a value to csp directive" do
Configuration.default do |config|
config.csp = {
Expand All @@ -169,6 +173,52 @@ module SecureHeaders
expect(hash[ContentSecurityPolicyConfig::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[ContentSecurityPolicyConfig::HEADER_NAME]).to eq("default-src 'self' https:; script-src 'self' https: 'unsafe-inline'")
end

it "appends a nonce to a missing script-src value" do
Configuration.default do |config|
config.csp = {
default_src: %w('self')
}
end

SecureHeaders.content_security_policy_script_nonce(request) # should add the value to the header
hash = SecureHeaders.header_hash_for(chrome_request)
expect(hash[ContentSecurityPolicyConfig::HEADER_NAME]).to match /\Adefault-src 'self'; script-src 'self' 'nonce-.*'\z/
end

it "appends a hash to a missing script-src value" do
Configuration.default do |config|
config.csp = {
default_src: %w('self')
}
end

SecureHeaders.append_content_security_policy_directives(request, script_src: %w('sha256-abc123'))
hash = SecureHeaders.header_hash_for(chrome_request)
expect(hash[ContentSecurityPolicyConfig::HEADER_NAME]).to match /\Adefault-src 'self'; script-src 'self' 'sha256-abc123'\z/
end

it "overrides individual directives" do
Configuration.default do |config|
config.csp = {
Expand Down Expand Up @@ -216,7 +266,6 @@ module SecureHeaders
}
end

chrome_request = Rack::Request.new(request.env.merge("HTTP_USER_AGENT" => USER_AGENTS[:chrome]))
nonce = SecureHeaders.content_security_policy_script_nonce(chrome_request)

# simulate the nonce being used multiple times in a request:
Expand Down

0 comments on commit 9a3c729

Please sign in to comment.