Skip to content

Commit

Permalink
Merge pull request #168 from twitter/hash-of-headers
Browse files Browse the repository at this point in the history
SecureHeaders as a rack middleware
  • Loading branch information
oreoshake committed Sep 30, 2015
2 parents 140d29e + 6ed1c41 commit 99db32e
Show file tree
Hide file tree
Showing 11 changed files with 137 additions and 21 deletions.
29 changes: 29 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -415,6 +415,35 @@ def before_load
end
```

### Using in rack middleware

The `SecureHeaders::header_hash` generates a hash of all header values, which is useful for merging with rack middleware values.

```ruby
class MySecureHeaders
include SecureHeaders
def initialize(app)
@app = app
end

def call(env)
status, headers, response = @app.call(env)
security_headers = if override?
SecureHeaders::header_hash(:csp => false) # uses global config, but overrides CSP config
else
SecureHeaders::header_hash # uses global config
end
[status, headers.merge(security_headers), [response.body]]
end
end

module Testapp
class Application < Rails::Application
config.middleware.use MySecureHeaders
end
end
```

## Similar libraries

* Rack [rack-secure_headers](https://github.com/harmoni/rack-secure_headers)
Expand Down
68 changes: 48 additions & 20 deletions lib/secure_headers.rb
Original file line number Diff line number Diff line change
@@ -1,7 +1,32 @@
require "secure_headers/version"
require "secure_headers/header"
require "secure_headers/headers/public_key_pins"
require "secure_headers/headers/content_security_policy"
require "secure_headers/headers/x_frame_options"
require "secure_headers/headers/strict_transport_security"
require "secure_headers/headers/x_xss_protection"
require "secure_headers/headers/x_content_type_options"
require "secure_headers/headers/x_download_options"
require "secure_headers/headers/x_permitted_cross_domain_policies"
require "secure_headers/railtie"
require "secure_headers/hash_helper"
require "secure_headers/view_helper"

module SecureHeaders
SCRIPT_HASH_CONFIG_FILE = 'config/script_hashes.yml'
HASHES_ENV_KEY = 'secure_headers.script_hashes'

ALL_HEADER_CLASSES = [
SecureHeaders::ContentSecurityPolicy,
SecureHeaders::StrictTransportSecurity,
SecureHeaders::PublicKeyPins,
SecureHeaders::XContentTypeOptions,
SecureHeaders::XDownloadOptions,
SecureHeaders::XFrameOptions,
SecureHeaders::XPermittedCrossDomainPolicies,
SecureHeaders::XXssProtection
]

module Configuration
class << self
attr_accessor :hsts, :x_frame_options, :x_content_type_options,
Expand All @@ -24,6 +49,27 @@ def append_features(base)
include InstanceMethods
end
end

def header_hash(options = nil)
ALL_HEADER_CLASSES.inject({}) do |memo, klass|
config = if options.is_a?(Hash) && options[klass::Constants::CONFIG_KEY]
options[klass::Constants::CONFIG_KEY]
else
::SecureHeaders::Configuration.send(klass::Constants::CONFIG_KEY)
end

unless klass == SecureHeaders::PublicKeyPins && !config.is_a?(Hash)
header = get_a_header(klass::Constants::CONFIG_KEY, klass, config)
memo[header.name] = header.value
end
memo
end
end

def get_a_header(name, klass, options)
return if options == false
klass.new(options)
end
end

module ClassMethods
Expand Down Expand Up @@ -161,13 +207,10 @@ def secure_header_options_for(type, options)
options.nil? ? ::SecureHeaders::Configuration.send(type) : options
end


def set_a_header(name, klass, options=nil)
options = secure_header_options_for name, options
options = secure_header_options_for(name, options)
return if options == false

header = klass.new(options)
set_header(header)
set_header(SecureHeaders::get_a_header(name, klass, options))
end

def set_header(name_or_header, value=nil)
Expand All @@ -180,18 +223,3 @@ def set_header(name_or_header, value=nil)
end
end
end


require "secure_headers/version"
require "secure_headers/header"
require "secure_headers/headers/public_key_pins"
require "secure_headers/headers/content_security_policy"
require "secure_headers/headers/x_frame_options"
require "secure_headers/headers/strict_transport_security"
require "secure_headers/headers/x_xss_protection"
require "secure_headers/headers/x_content_type_options"
require "secure_headers/headers/x_download_options"
require "secure_headers/headers/x_permitted_cross_domain_policies"
require "secure_headers/railtie"
require "secure_headers/hash_helper"
require "secure_headers/view_helper"
2 changes: 2 additions & 0 deletions lib/secure_headers/headers/content_security_policy.rb
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,9 @@ module Constants
SOURCE_DIRECTIVES = DIRECTIVES + NON_DEFAULT_SOURCES

ALL_DIRECTIVES = DIRECTIVES + NON_DEFAULT_SOURCES + OTHER
CONFIG_KEY = :csp
end

include Constants

attr_reader :disable_fill_missing, :ssl_request
Expand Down
1 change: 1 addition & 0 deletions lib/secure_headers/headers/public_key_pins.rb
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ module Constants
ENV_KEY = 'secure_headers.public_key_pins'
HASH_ALGORITHMS = [:sha256]
DIRECTIVES = [:max_age]
CONFIG_KEY = :hpkp
end
class << self
def symbol_to_hyphen_case sym
Expand Down
1 change: 1 addition & 0 deletions lib/secure_headers/headers/strict_transport_security.rb
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ module Constants
DEFAULT_VALUE = "max-age=" + HSTS_MAX_AGE
VALID_STS_HEADER = /\Amax-age=\d+(; includeSubdomains)?(; preload)?\z/i
MESSAGE = "The config value supplied for the HSTS header was invalid."
CONFIG_KEY = :hsts
end
include Constants

Expand Down
3 changes: 2 additions & 1 deletion lib/secure_headers/headers/x_content_type_options.rb
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ class XContentTypeOptions < Header
module Constants
X_CONTENT_TYPE_OPTIONS_HEADER_NAME = "X-Content-Type-Options"
DEFAULT_VALUE = "nosniff"
CONFIG_KEY = :x_content_type_options
end
include Constants

Expand Down Expand Up @@ -37,4 +38,4 @@ def validate_config
end
end
end
end
end
1 change: 1 addition & 0 deletions lib/secure_headers/headers/x_download_options.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ class XDownloadOptions < Header
module Constants
XDO_HEADER_NAME = "X-Download-Options"
DEFAULT_VALUE = 'noopen'
CONFIG_KEY = :x_download_options
end
include Constants

Expand Down
1 change: 1 addition & 0 deletions lib/secure_headers/headers/x_frame_options.rb
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ module Constants
XFO_HEADER_NAME = "X-Frame-Options"
DEFAULT_VALUE = 'SAMEORIGIN'
VALID_XFO_HEADER = /\A(SAMEORIGIN\z|DENY\z|ALLOW-FROM[:\s])/i
CONFIG_KEY = :x_frame_options
end
include Constants

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ module Constants
XPCDP_HEADER_NAME = "X-Permitted-Cross-Domain-Policies"
DEFAULT_VALUE = 'none'
VALID_POLICIES = %w(all none master-only by-content-type by-ftp-filename)
CONFIG_KEY = :x_permitted_cross_domain_policies
end
include Constants

Expand Down
1 change: 1 addition & 0 deletions lib/secure_headers/headers/x_xss_protection.rb
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ module Constants
X_XSS_PROTECTION_HEADER_NAME = 'X-XSS-Protection'
DEFAULT_VALUE = "1"
VALID_X_XSS_HEADER = /\A[01](; mode=block)?(; report=.*)?\z/i
CONFIG_KEY = :x_xss_protection
end
include Constants

Expand Down
50 changes: 50 additions & 0 deletions spec/lib/secure_headers_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,56 @@ def set_security_headers(subject)
end
end

describe "SecureHeaders#header_hash" do
def expect_default_values(hash)
expect(hash[XFO_HEADER_NAME]).to eq(SecureHeaders::XFrameOptions::Constants::DEFAULT_VALUE)
expect(hash[XDO_HEADER_NAME]).to eq(SecureHeaders::XDownloadOptions::Constants::DEFAULT_VALUE)
expect(hash[HSTS_HEADER_NAME]).to eq(SecureHeaders::StrictTransportSecurity::Constants::DEFAULT_VALUE)
expect(hash[X_XSS_PROTECTION_HEADER_NAME]).to eq(SecureHeaders::XXssProtection::Constants::DEFAULT_VALUE)
expect(hash[X_CONTENT_TYPE_OPTIONS_HEADER_NAME]).to eq(SecureHeaders::XContentTypeOptions::Constants::DEFAULT_VALUE)
expect(hash[XPCDP_HEADER_NAME]).to eq(SecureHeaders::XPermittedCrossDomainPolicies::Constants::DEFAULT_VALUE)
end

it "produces a hash of headers given a hash as config" do
hash = SecureHeaders::header_hash(:csp => {:default_src => 'none', :img_src => "data:", :disable_fill_missing => true})
expect(hash['Content-Security-Policy-Report-Only']).to eq("default-src 'none'; img-src data:;")
expect_default_values(hash)
end

it "produces a hash with a mix of config values, override values, and default values" do
::SecureHeaders::Configuration.configure do |config|
config.hsts = { :max_age => '123456'}
config.hpkp = {
:enforce => true,
:max_age => 1000000,
:include_subdomains => true,
:report_uri => '//example.com/uri-directive',
:pins => [
{:sha256 => 'abc'},
{:sha256 => '123'}
]
}
end

hash = SecureHeaders::header_hash(:csp => {:default_src => 'none', :img_src => "data:", :disable_fill_missing => true})
::SecureHeaders::Configuration.configure do |config|
config.hsts = nil
config.hpkp = nil
end

expect(hash['Content-Security-Policy-Report-Only']).to eq("default-src 'none'; img-src data:;")
expect(hash[XFO_HEADER_NAME]).to eq(SecureHeaders::XFrameOptions::Constants::DEFAULT_VALUE)
expect(hash[HSTS_HEADER_NAME]).to eq("max-age=123456")
expect(hash[HPKP_HEADER_NAME]).to eq(%{max-age=1000000; pin-sha256="abc"; pin-sha256="123"; report-uri="//example.com/uri-directive"; includeSubDomains})
end

it "produces a hash of headers with default config" do
hash = SecureHeaders::header_hash
expect(hash['Content-Security-Policy-Report-Only']).to eq(SecureHeaders::ContentSecurityPolicy::Constants::DEFAULT_CSP_HEADER)
expect_default_values(hash)
end
end

describe "#set_x_frame_options_header" do
it "sets the X-Frame-Options header" do
should_assign_header(XFO_HEADER_NAME, SecureHeaders::XFrameOptions::Constants::DEFAULT_VALUE)
Expand Down

0 comments on commit 99db32e

Please sign in to comment.