Skip to content

SecureHeaders as a rack middleware #168

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

Merged
merged 7 commits into from
Sep 30, 2015
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.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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