secure_headers 1.3.3 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/.gitignore +4 -8
- data/.travis.yml +7 -4
- data/Gemfile +6 -7
- data/README.md +115 -79
- data/Rakefile +11 -116
- data/fixtures/rails_3_2_12/Gemfile +0 -6
- data/fixtures/rails_3_2_12/app/controllers/things_controller.rb +0 -1
- data/fixtures/rails_3_2_12/app/views/layouts/application.html.erb +1 -4
- data/fixtures/rails_3_2_12/app/views/other_things/index.html.erb +2 -1
- data/fixtures/rails_3_2_12/app/views/things/index.html.erb +1 -21
- data/fixtures/rails_3_2_12/config/application.rb +0 -54
- data/fixtures/rails_3_2_12/config/environments/test.rb +2 -2
- data/fixtures/rails_3_2_12/config/initializers/secure_headers.rb +2 -1
- data/fixtures/rails_3_2_12/config/routes.rb +0 -57
- data/fixtures/rails_3_2_12/config/script_hashes.yml +5 -0
- data/fixtures/rails_3_2_12/config.ru +3 -0
- data/fixtures/rails_3_2_12/spec/controllers/other_things_controller_spec.rb +58 -16
- data/fixtures/rails_3_2_12/spec/controllers/things_controller_spec.rb +11 -1
- data/fixtures/rails_3_2_12_no_init/Gemfile +0 -7
- data/fixtures/rails_3_2_12_no_init/app/controllers/other_things_controller.rb +1 -2
- data/fixtures/rails_3_2_12_no_init/app/controllers/things_controller.rb +1 -1
- data/fixtures/rails_3_2_12_no_init/app/views/layouts/application.html.erb +0 -2
- data/fixtures/rails_3_2_12_no_init/app/views/things/index.html.erb +0 -21
- data/fixtures/rails_3_2_12_no_init/config/application.rb +0 -51
- data/fixtures/rails_3_2_12_no_init/config/environments/test.rb +2 -2
- data/fixtures/rails_3_2_12_no_init/config/routes.rb +0 -57
- data/fixtures/rails_3_2_12_no_init/spec/controllers/other_things_controller_spec.rb +10 -0
- data/fixtures/rails_3_2_12_no_init/spec/controllers/things_controller_spec.rb +10 -0
- data/fixtures/rails_4_1_8/Gemfile +5 -0
- data/fixtures/rails_4_1_8/README.rdoc +28 -0
- data/fixtures/rails_4_1_8/Rakefile +6 -0
- data/fixtures/rails_4_1_8/app/controllers/application_controller.rb +4 -0
- data/fixtures/rails_4_1_8/app/controllers/concerns/.keep +0 -0
- data/fixtures/rails_4_1_8/app/controllers/other_things_controller.rb +5 -0
- data/fixtures/rails_4_1_8/app/controllers/things_controller.rb +5 -0
- data/fixtures/rails_4_1_8/app/models/.keep +0 -0
- data/fixtures/rails_4_1_8/app/models/concerns/.keep +0 -0
- data/fixtures/rails_4_1_8/app/views/layouts/application.html.erb +11 -0
- data/fixtures/rails_4_1_8/app/views/other_things/index.html.erb +2 -0
- data/fixtures/rails_4_1_8/app/views/things/index.html.erb +1 -0
- data/fixtures/rails_4_1_8/config/application.rb +15 -0
- data/fixtures/rails_4_1_8/config/boot.rb +4 -0
- data/fixtures/rails_4_1_8/config/environment.rb +5 -0
- data/fixtures/rails_4_1_8/config/environments/test.rb +10 -0
- data/fixtures/rails_4_1_8/config/initializers/secure_headers.rb +17 -0
- data/fixtures/rails_4_1_8/config/routes.rb +4 -0
- data/fixtures/rails_4_1_8/config/script_hashes.yml +5 -0
- data/fixtures/rails_4_1_8/config/secrets.yml +22 -0
- data/fixtures/rails_4_1_8/config.ru +4 -0
- data/fixtures/rails_4_1_8/lib/assets/.keep +0 -0
- data/fixtures/rails_4_1_8/lib/tasks/.keep +0 -0
- data/fixtures/rails_4_1_8/log/.keep +0 -0
- data/fixtures/rails_4_1_8/spec/controllers/other_things_controller_spec.rb +83 -0
- data/fixtures/rails_4_1_8/spec/controllers/things_controller_spec.rb +59 -0
- data/fixtures/rails_4_1_8/spec/spec_helper.rb +15 -0
- data/fixtures/rails_4_1_8/vendor/assets/javascripts/.keep +0 -0
- data/fixtures/rails_4_1_8/vendor/assets/stylesheets/.keep +0 -0
- data/lib/secure_headers/hash_helper.rb +7 -0
- data/lib/secure_headers/headers/content_security_policy/script_hash_middleware.rb +22 -0
- data/lib/secure_headers/headers/content_security_policy.rb +141 -133
- data/lib/secure_headers/headers/x_download_options.rb +39 -0
- data/lib/secure_headers/headers/x_permitted_cross_domain_policies.rb +40 -0
- data/lib/secure_headers/headers/x_xss_protection.rb +2 -1
- data/lib/secure_headers/railtie.rb +19 -24
- data/lib/secure_headers/version.rb +1 -1
- data/lib/secure_headers/view_helper.rb +68 -0
- data/lib/secure_headers.rb +65 -16
- data/lib/tasks/tasks.rake +48 -0
- data/secure_headers.gemspec +4 -4
- data/spec/lib/secure_headers/headers/content_security_policy/script_hash_middleware_spec.rb +47 -0
- data/spec/lib/secure_headers/headers/content_security_policy_spec.rb +83 -208
- data/spec/lib/secure_headers/headers/x_download_options_spec.rb +32 -0
- data/spec/lib/secure_headers/headers/x_permitted_cross_domain_policies_spec.rb +64 -0
- data/spec/lib/secure_headers/headers/x_xss_protection_spec.rb +2 -1
- data/spec/lib/secure_headers_spec.rb +57 -61
- data/spec/spec_helper.rb +27 -1
- metadata +47 -42
- data/Guardfile +0 -6
- data/HISTORY.md +0 -155
- data/app/controllers/content_security_policy_controller.rb +0 -75
- data/config/curl-ca-bundle.crt +0 -5420
- data/config/routes.rb +0 -3
- data/fixtures/rails_3_2_12/app/models/thing.rb +0 -3
- data/fixtures/rails_3_2_12/config/database.yml +0 -25
- data/fixtures/rails_3_2_12/config/environments/development.rb +0 -37
- data/fixtures/rails_3_2_12/config/environments/production.rb +0 -67
- data/fixtures/rails_3_2_12/config/initializers/backtrace_silencers.rb +0 -7
- data/fixtures/rails_3_2_12/config/initializers/inflections.rb +0 -15
- data/fixtures/rails_3_2_12/config/initializers/mime_types.rb +0 -5
- data/fixtures/rails_3_2_12/config/initializers/secret_token.rb +0 -7
- data/fixtures/rails_3_2_12/config/initializers/session_store.rb +0 -8
- data/fixtures/rails_3_2_12/config/initializers/wrap_parameters.rb +0 -14
- data/fixtures/rails_3_2_12/config/locales/en.yml +0 -5
- data/fixtures/rails_3_2_12/db/schema.rb +0 -16
- data/fixtures/rails_3_2_12/db/seeds.rb +0 -7
- data/fixtures/rails_3_2_12_no_init/app/models/thing.rb +0 -3
- data/fixtures/rails_3_2_12_no_init/app/views/things/_form.html.erb +0 -17
- data/fixtures/rails_3_2_12_no_init/app/views/things/edit.html.erb +0 -6
- data/fixtures/rails_3_2_12_no_init/app/views/things/new.html.erb +0 -5
- data/fixtures/rails_3_2_12_no_init/app/views/things/show.html.erb +0 -5
- data/fixtures/rails_3_2_12_no_init/config/database.yml +0 -25
- data/fixtures/rails_3_2_12_no_init/config/environments/development.rb +0 -37
- data/fixtures/rails_3_2_12_no_init/config/environments/production.rb +0 -67
- data/fixtures/rails_3_2_12_no_init/config/initializers/backtrace_silencers.rb +0 -7
- data/fixtures/rails_3_2_12_no_init/config/initializers/inflections.rb +0 -15
- data/fixtures/rails_3_2_12_no_init/config/initializers/mime_types.rb +0 -5
- data/fixtures/rails_3_2_12_no_init/config/initializers/secret_token.rb +0 -7
- data/fixtures/rails_3_2_12_no_init/config/initializers/session_store.rb +0 -8
- data/fixtures/rails_3_2_12_no_init/config/initializers/wrap_parameters.rb +0 -14
- data/fixtures/rails_3_2_12_no_init/config/locales/en.yml +0 -5
- data/fixtures/rails_3_2_12_no_init/db/schema.rb +0 -16
- data/fixtures/rails_3_2_12_no_init/db/seeds.rb +0 -7
- data/spec/controllers/content_security_policy_controller_spec.rb +0 -90
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# Be sure to restart your server when you modify this file.
|
|
2
|
+
|
|
3
|
+
# Your secret key is used for verifying the integrity of signed cookies.
|
|
4
|
+
# If you change this key, all old signed cookies will become invalid!
|
|
5
|
+
|
|
6
|
+
# Make sure the secret is at least 30 characters and all random,
|
|
7
|
+
# no regular words or you'll be exposed to dictionary attacks.
|
|
8
|
+
# You can use `rake secret` to generate a secure secret key.
|
|
9
|
+
|
|
10
|
+
# Make sure the secrets in this file are kept private
|
|
11
|
+
# if you're sharing your code publicly.
|
|
12
|
+
|
|
13
|
+
development:
|
|
14
|
+
secret_key_base: ddba38f932720d8f18257f2a05dc278963a29cf569c45aa97ff4e9fc9bbc78af5a03fcf135caad45caee66ac09f8f9913c1f5e338a61213f420eefa8dd6363d2
|
|
15
|
+
|
|
16
|
+
test:
|
|
17
|
+
secret_key_base: f73abd7eab84fa7af5a2fc0a9c2727c5bad47433e51aa0c9c6b0782dac176a8e7f337e1f93adc6d6fc17027e67a533040b6408e54d72dea2eec6e5b9820dbcb9
|
|
18
|
+
|
|
19
|
+
# Do not keep production secrets in the repository,
|
|
20
|
+
# instead read values from the environment.
|
|
21
|
+
production:
|
|
22
|
+
secret_key_base: <%= ENV["SECRET_KEY_BASE"] %>
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
require 'spec_helper'
|
|
2
|
+
|
|
3
|
+
require 'secure_headers/headers/content_security_policy/script_hash_middleware'
|
|
4
|
+
|
|
5
|
+
describe OtherThingsController, :type => :controller do
|
|
6
|
+
include Rack::Test::Methods
|
|
7
|
+
|
|
8
|
+
def app
|
|
9
|
+
OtherThingsController.action(:index)
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def request(opts = {})
|
|
13
|
+
options = opts.merge(
|
|
14
|
+
{
|
|
15
|
+
'HTTPS' => 'on',
|
|
16
|
+
'HTTP_USER_AGENT' => "Mozilla/5.0 (compatible; MSIE 10.6; Windows NT 6.1; Trident/5.0; InfoPath.2; SLCC1; .NET CLR 3.0.4506.2152; .NET CLR 3.5.30729; .NET CLR 2.0.50727) 3gpp-gba UNTRUSTED/1.0"
|
|
17
|
+
}
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
Rack::MockRequest.env_for('/', options)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
describe "headers" do
|
|
26
|
+
before(:each) do
|
|
27
|
+
_, @env = app.call(request)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
it "sets the X-XSS-Protection header" do
|
|
31
|
+
get '/'
|
|
32
|
+
expect(@env['X-XSS-Protection']).to eq('0')
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
it "sets the X-Frame-Options header" do
|
|
36
|
+
get '/'
|
|
37
|
+
expect(@env['X-Frame-Options']).to eq('DENY')
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
it "sets the CSP header with a local reference to a nonce" do
|
|
41
|
+
middleware = ::SecureHeaders::ContentSecurityPolicy::ScriptHashMiddleware.new(app)
|
|
42
|
+
_, env = middleware.call(request(@env))
|
|
43
|
+
expect(env['Content-Security-Policy-Report-Only']).to match(/script-src[^;]*'nonce-[a-zA-Z0-9\+\/=]{44}'/)
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
it "sets the required hashes to whitelist inline script" do
|
|
47
|
+
middleware = ::SecureHeaders::ContentSecurityPolicy::ScriptHashMiddleware.new(app)
|
|
48
|
+
_, env = middleware.call(request(@env))
|
|
49
|
+
hashes = ['sha256-VjDxT7saxd2FgaUQQTWw/jsTnvonaoCP/ACWDBTpyhU=', 'sha256-ZXAcP8a0y1pPMTJW8pUr43c+XBkgYQBwHOPvXk9mq5A=']
|
|
50
|
+
hashes.each do |hash|
|
|
51
|
+
expect(env['Content-Security-Policy-Report-Only']).to include(hash)
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
it "sets the Strict-Transport-Security header" do
|
|
56
|
+
get '/'
|
|
57
|
+
expect(@env['Strict-Transport-Security']).to eq("max-age=315576000")
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
it "sets the X-Download-Options header" do
|
|
61
|
+
get '/'
|
|
62
|
+
expect(@env['X-Download-Options']).to eq('noopen')
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
it "sets the X-Content-Type-Options header" do
|
|
66
|
+
get '/'
|
|
67
|
+
expect(@env['X-Content-Type-Options']).to eq("nosniff")
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
it "sets the X-Permitted-Cross-Domain-Policies" do
|
|
71
|
+
get '/'
|
|
72
|
+
expect(@env['X-Permitted-Cross-Domain-Policies']).to eq("none")
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
context "using IE" do
|
|
76
|
+
it "sets the X-Content-Type-Options header" do
|
|
77
|
+
@env['HTTP_USER_AGENT'] = "Mozilla/5.0 (compatible; MSIE 10.6; Windows NT 6.1; Trident/5.0; InfoPath.2; SLCC1; .NET CLR 3.0.4506.2152; .NET CLR 3.5.30729; .NET CLR 2.0.50727) 3gpp-gba UNTRUSTED/1.0"
|
|
78
|
+
get '/'
|
|
79
|
+
expect(@env['X-Content-Type-Options']).to eq("nosniff")
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
end
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
# config.action_dispatch.default_headers defaults to:
|
|
2
|
+
# {"X-Frame-Options"=>"SAMEORIGIN", "X-XSS-Protection"=>"1; mode=block", "X-Content-Type-Options"=>"nosniff"}
|
|
3
|
+
# so we want to set our specs to expect something else to ensure secureheaders is taking precedence
|
|
4
|
+
|
|
5
|
+
require 'spec_helper'
|
|
6
|
+
|
|
7
|
+
# This controller is meant to be something that inherits config from application controller
|
|
8
|
+
# all values are defaulted because no initializer is configured, and the values in app controller
|
|
9
|
+
# only provide csp => false
|
|
10
|
+
|
|
11
|
+
describe ThingsController, :type => :controller do
|
|
12
|
+
|
|
13
|
+
describe "headers" do
|
|
14
|
+
it "sets the X-XSS-Protection header" do
|
|
15
|
+
get :index
|
|
16
|
+
expect(response.headers['X-XSS-Protection']).to eq('0')
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
it "sets the X-Frame-Options header" do
|
|
20
|
+
get :index
|
|
21
|
+
expect(response.headers['X-Frame-Options']).to eq('DENY')
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
it "does not set CSP header" do
|
|
25
|
+
get :index
|
|
26
|
+
expect(response.headers['Content-Security-Policy-Report-Only']).to eq(nil)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
#mock ssl
|
|
30
|
+
it "sets the Strict-Transport-Security header" do
|
|
31
|
+
request.env['HTTPS'] = 'on'
|
|
32
|
+
get :index
|
|
33
|
+
expect(response.headers['Strict-Transport-Security']).to eq("max-age=315576000")
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
it "sets the X-Download-Options header" do
|
|
37
|
+
get :index
|
|
38
|
+
expect(response.headers['X-Download-Options']).to eq('noopen')
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
it "sets the X-Content-Type-Options header" do
|
|
42
|
+
get :index
|
|
43
|
+
expect(response.headers['X-Content-Type-Options']).to eq("nosniff")
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
it "sets the X-Permitted-Cross-Domain-Policies" do
|
|
47
|
+
get :index
|
|
48
|
+
expect(response.headers['X-Permitted-Cross-Domain-Policies']).to eq("none")
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
context "using IE" do
|
|
52
|
+
it "sets the X-Content-Type-Options header" do
|
|
53
|
+
request.env['HTTP_USER_AGENT'] = "Mozilla/5.0 (compatible; MSIE 10.6; Windows NT 6.1; Trident/5.0; InfoPath.2; SLCC1; .NET CLR 3.0.4506.2152; .NET CLR 3.5.30729; .NET CLR 2.0.50727) 3gpp-gba UNTRUSTED/1.0"
|
|
54
|
+
get :index
|
|
55
|
+
expect(response.headers['X-Content-Type-Options']).to eq("nosniff")
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
require 'rubygems'
|
|
2
|
+
|
|
3
|
+
#uncomment the following line to use spork with the debugger
|
|
4
|
+
#require 'spork/ext/ruby-debug'
|
|
5
|
+
|
|
6
|
+
# Spork.prefork do
|
|
7
|
+
# Loading more in this block will cause your tests to run faster. However,
|
|
8
|
+
# if you change any configuration or code from libraries loaded here, you'll
|
|
9
|
+
# need to restart spork for it take effect.
|
|
10
|
+
# This file is copied to spec/ when you run 'rails generate rspec:install'
|
|
11
|
+
ENV["RAILS_ENV"] ||= 'test'
|
|
12
|
+
require File.expand_path("../../config/environment", __FILE__)
|
|
13
|
+
require 'rspec/rails'
|
|
14
|
+
# end
|
|
15
|
+
|
|
File without changes
|
|
File without changes
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
module SecureHeaders
|
|
2
|
+
class ContentSecurityPolicy
|
|
3
|
+
class ScriptHashMiddleware
|
|
4
|
+
def initialize(app)
|
|
5
|
+
@app = app
|
|
6
|
+
end
|
|
7
|
+
|
|
8
|
+
def call(env)
|
|
9
|
+
status, headers, response = @app.call(env)
|
|
10
|
+
metadata = env[ContentSecurityPolicy::ENV_KEY]
|
|
11
|
+
if !metadata.nil?
|
|
12
|
+
config, options = metadata.values_at(:config, :options)
|
|
13
|
+
config.merge!(:script_hashes => env[HASHES_ENV_KEY])
|
|
14
|
+
csp_header = ContentSecurityPolicy.new(config, options)
|
|
15
|
+
headers[csp_header.name] = csp_header.value
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
[status, headers, response]
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
@@ -1,90 +1,154 @@
|
|
|
1
1
|
require 'uri'
|
|
2
2
|
require 'base64'
|
|
3
|
+
require 'securerandom'
|
|
3
4
|
|
|
4
5
|
module SecureHeaders
|
|
5
6
|
class ContentSecurityPolicyBuildError < StandardError; end
|
|
6
7
|
class ContentSecurityPolicy < Header
|
|
7
8
|
module Constants
|
|
8
|
-
DEFAULT_CSP_HEADER = "default-src https: data: 'unsafe-inline' 'unsafe-eval'; frame-src https
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
DIRECTIVES = [
|
|
12
|
-
|
|
9
|
+
DEFAULT_CSP_HEADER = "default-src https: data: 'unsafe-inline' 'unsafe-eval'; frame-src https: about: javascript:; img-src data:"
|
|
10
|
+
HEADER_NAME = "Content-Security-Policy"
|
|
11
|
+
ENV_KEY = 'secure_headers.content_security_policy'
|
|
12
|
+
DIRECTIVES = [
|
|
13
|
+
:default_src,
|
|
14
|
+
:connect_src,
|
|
15
|
+
:font_src,
|
|
16
|
+
:frame_src,
|
|
17
|
+
:img_src,
|
|
18
|
+
:media_src,
|
|
19
|
+
:object_src,
|
|
20
|
+
:script_src,
|
|
21
|
+
:style_src
|
|
22
|
+
]
|
|
23
|
+
|
|
24
|
+
NON_DEFAULT_SOURCES = [
|
|
25
|
+
:base_uri,
|
|
26
|
+
:child_src,
|
|
27
|
+
:form_action,
|
|
28
|
+
:frame_ancestors,
|
|
29
|
+
:plugin_types,
|
|
30
|
+
:referrer,
|
|
31
|
+
:reflected_xss
|
|
32
|
+
]
|
|
33
|
+
|
|
34
|
+
ALL_DIRECTIVES = DIRECTIVES + NON_DEFAULT_SOURCES
|
|
13
35
|
end
|
|
14
36
|
include Constants
|
|
15
37
|
|
|
16
|
-
|
|
17
|
-
attr_reader :browser, :ssl_request, :report_uri, :request_uri, :experimental
|
|
18
|
-
|
|
19
|
-
alias :disable_chrome_extension? :disable_chrome_extension
|
|
38
|
+
attr_reader :disable_fill_missing, :ssl_request
|
|
20
39
|
alias :disable_fill_missing? :disable_fill_missing
|
|
21
40
|
alias :ssl_request? :ssl_request
|
|
22
41
|
|
|
42
|
+
class << self
|
|
43
|
+
def generate_nonce
|
|
44
|
+
SecureRandom.base64(32).chomp
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def set_nonce(controller, nonce = generate_nonce)
|
|
48
|
+
controller.instance_variable_set(:@content_security_policy_nonce, nonce)
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def add_to_env(request, controller, config)
|
|
52
|
+
set_nonce(controller)
|
|
53
|
+
options = options_from_request(request).merge(:controller => controller)
|
|
54
|
+
request.env[Constants::ENV_KEY] = {
|
|
55
|
+
:config => config,
|
|
56
|
+
:options => options,
|
|
57
|
+
}
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def options_from_request(request)
|
|
61
|
+
{
|
|
62
|
+
:ssl => request.ssl?,
|
|
63
|
+
:ua => request.env['HTTP_USER_AGENT'],
|
|
64
|
+
:request_uri => request_uri_from_request(request),
|
|
65
|
+
}
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def request_uri_from_request(request)
|
|
69
|
+
if request.respond_to?(:original_url)
|
|
70
|
+
# rails 3.1+
|
|
71
|
+
request.original_url
|
|
72
|
+
else
|
|
73
|
+
# rails 2/3.0
|
|
74
|
+
request.url
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def symbol_to_hyphen_case sym
|
|
79
|
+
sym.to_s.gsub('_', '-')
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
|
|
23
83
|
# +options+ param contains
|
|
24
|
-
# :
|
|
84
|
+
# :controller used for setting instance variables for nonces/hashes
|
|
25
85
|
# :ssl_request used to determine if http_additions should be used
|
|
26
|
-
# :request_uri used to determine if firefox should send the report directly
|
|
27
|
-
# or use the forwarding endpoint
|
|
28
86
|
# :ua the user agent (or just use Firefox/Chrome/MSIE/etc)
|
|
29
87
|
#
|
|
30
88
|
# :report used to determine what :ssl_request, :ua, and :request_uri are set to
|
|
31
89
|
def initialize(config=nil, options={})
|
|
32
|
-
|
|
33
|
-
@controller = options.delete(:controller)
|
|
90
|
+
return unless config
|
|
34
91
|
|
|
35
92
|
if options[:request]
|
|
36
|
-
|
|
37
|
-
else
|
|
38
|
-
@ua = options[:ua]
|
|
39
|
-
# fails open, assumes http. Bad idea? Will always include http additions.
|
|
40
|
-
# could also fail if not supplied.
|
|
41
|
-
@ssl_request = !!options.delete(:ssl)
|
|
42
|
-
# a nil value here means we always assume we are not on the same host,
|
|
43
|
-
# which causes all FF csp reports to go through the forwarder
|
|
44
|
-
@request_uri = options.delete(:request_uri)
|
|
93
|
+
options = options.merge(self.class.options_from_request(options[:request]))
|
|
45
94
|
end
|
|
46
95
|
|
|
47
|
-
|
|
48
|
-
|
|
96
|
+
@controller = options[:controller]
|
|
97
|
+
@ua = options[:ua]
|
|
98
|
+
@ssl_request = !!options.delete(:ssl)
|
|
99
|
+
@request_uri = options.delete(:request_uri)
|
|
49
100
|
|
|
50
|
-
|
|
51
|
-
@
|
|
52
|
-
|
|
101
|
+
# Config values can be string, array, or lamdba values
|
|
102
|
+
@config = config.inject({}) do |hash, (key, value)|
|
|
103
|
+
config_val = value.respond_to?(:call) ? value.call : value
|
|
53
104
|
|
|
54
|
-
|
|
55
|
-
|
|
105
|
+
if ALL_DIRECTIVES.include?(key) # directives need to be normalized to arrays of strings
|
|
106
|
+
config_val = config_val.split if config_val.is_a? String
|
|
107
|
+
if config_val.is_a?(Array)
|
|
108
|
+
config_val = config_val.map do |val|
|
|
109
|
+
translate_dir_value(val)
|
|
110
|
+
end.flatten.uniq
|
|
111
|
+
end
|
|
112
|
+
end
|
|
56
113
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
@config[:http_additions] = experimental_config[:http_additions]
|
|
60
|
-
@config.merge!(experimental_config)
|
|
114
|
+
hash[key] = config_val
|
|
115
|
+
hash
|
|
61
116
|
end
|
|
62
117
|
|
|
63
|
-
# these values don't support lambdas because this needs to be rewritten
|
|
64
118
|
@http_additions = @config.delete(:http_additions)
|
|
65
119
|
@app_name = @config.delete(:app_name)
|
|
120
|
+
@report_uri = @config.delete(:report_uri)
|
|
66
121
|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
META.each do |meta|
|
|
70
|
-
self.send("#{meta}=", @config.delete(meta))
|
|
71
|
-
end
|
|
72
|
-
|
|
122
|
+
@disable_fill_missing = !!@config.delete(:disable_fill_missing)
|
|
73
123
|
@enforce = !!@config.delete(:enforce)
|
|
74
|
-
@tag_report_uri =
|
|
124
|
+
@tag_report_uri = !!@config.delete(:tag_report_uri)
|
|
125
|
+
@script_hashes = @config.delete(:script_hashes) || []
|
|
75
126
|
|
|
76
|
-
|
|
127
|
+
add_script_hashes if @script_hashes.any?
|
|
77
128
|
fill_directives unless disable_fill_missing?
|
|
78
129
|
end
|
|
79
130
|
|
|
131
|
+
##
|
|
132
|
+
# Return or initialize the nonce value used for this header.
|
|
133
|
+
# If a reference to a controller is passed in the config, this method
|
|
134
|
+
# will check if a nonce has already been set and use it.
|
|
135
|
+
def nonce
|
|
136
|
+
@nonce ||= @controller.instance_variable_get(:@content_security_policy_nonce) || self.class.generate_nonce
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
##
|
|
140
|
+
# Returns the name to use for the header. Either "Content-Security-Policy" or
|
|
141
|
+
# "Content-Security-Policy-Report-Only"
|
|
80
142
|
def name
|
|
81
|
-
base =
|
|
82
|
-
if !@enforce
|
|
143
|
+
base = HEADER_NAME
|
|
144
|
+
if !@enforce
|
|
83
145
|
base += "-Report-Only"
|
|
84
146
|
end
|
|
85
147
|
base
|
|
86
148
|
end
|
|
87
149
|
|
|
150
|
+
##
|
|
151
|
+
# Return the value of the CSP header
|
|
88
152
|
def value
|
|
89
153
|
return @config if @config.is_a?(String)
|
|
90
154
|
if @config
|
|
@@ -96,26 +160,28 @@ module SecureHeaders
|
|
|
96
160
|
|
|
97
161
|
private
|
|
98
162
|
|
|
163
|
+
def add_script_hashes
|
|
164
|
+
@config[:script_src] << @script_hashes.map {|hash| "'#{hash}'"} << ["'unsafe-inline'"]
|
|
165
|
+
end
|
|
166
|
+
|
|
99
167
|
def build_value
|
|
100
168
|
raise "Expected to find default_src directive value" unless @config[:default_src]
|
|
101
169
|
append_http_additions unless ssl_request?
|
|
102
170
|
header_value = [
|
|
103
|
-
generic_directives
|
|
171
|
+
generic_directives,
|
|
172
|
+
non_default_directives,
|
|
104
173
|
report_uri_directive
|
|
105
174
|
].join.strip
|
|
106
|
-
rescue StandardError => e
|
|
107
|
-
raise ContentSecurityPolicyBuildError.new("Couldn't build CSP header :( #{e}")
|
|
108
175
|
end
|
|
109
176
|
|
|
110
177
|
def fill_directives
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
178
|
+
if default = @config[:default_src]
|
|
179
|
+
DIRECTIVES.each do |directive|
|
|
180
|
+
unless @config[directive]
|
|
181
|
+
@config[directive] = default
|
|
182
|
+
end
|
|
116
183
|
end
|
|
117
184
|
end
|
|
118
|
-
@config
|
|
119
185
|
end
|
|
120
186
|
|
|
121
187
|
def append_http_additions
|
|
@@ -126,74 +192,20 @@ module SecureHeaders
|
|
|
126
192
|
end
|
|
127
193
|
end
|
|
128
194
|
|
|
129
|
-
def normalize_csp_options
|
|
130
|
-
@config = @config.inject({}) do |hash, (key, value)|
|
|
131
|
-
# lambdas
|
|
132
|
-
config_val = value.respond_to?(:call) ? value.call : value
|
|
133
|
-
# space-delimeted strings
|
|
134
|
-
config_val = config_val.split if config_val.is_a? String
|
|
135
|
-
# array of strings
|
|
136
|
-
if config_val.respond_to?(:map) #skip booleans
|
|
137
|
-
config_val = config_val.map do |val|
|
|
138
|
-
translate_dir_value(val)
|
|
139
|
-
end.flatten.uniq
|
|
140
|
-
end
|
|
141
|
-
|
|
142
|
-
hash[key] = config_val
|
|
143
|
-
hash
|
|
144
|
-
end
|
|
145
|
-
|
|
146
|
-
@report_uri = @config.delete(:report_uri).join(" ") if @config[:report_uri]
|
|
147
|
-
end
|
|
148
|
-
|
|
149
|
-
# translates 'inline','self', 'none' and 'eval' to their respective impl-specific values.
|
|
150
195
|
def translate_dir_value val
|
|
151
196
|
if %w{inline eval}.include?(val)
|
|
152
197
|
val == 'inline' ? "'unsafe-inline'" : "'unsafe-eval'"
|
|
153
|
-
# self/none are special sources/src-dir-values and need to be quoted
|
|
198
|
+
# self/none are special sources/src-dir-values and need to be quoted
|
|
154
199
|
elsif %{self none}.include?(val)
|
|
155
200
|
"'#{val}'"
|
|
156
201
|
elsif val == 'nonce'
|
|
157
|
-
@controller
|
|
202
|
+
self.class.set_nonce(@controller, nonce)
|
|
158
203
|
["'nonce-#{nonce}'", "'unsafe-inline'"]
|
|
159
204
|
else
|
|
160
205
|
val
|
|
161
206
|
end
|
|
162
207
|
end
|
|
163
208
|
|
|
164
|
-
# if we have a forwarding endpoint setup and we are not on the same origin as our report_uri
|
|
165
|
-
# or only a path was supplied (in which case we assume cross-host)
|
|
166
|
-
# we need to forward the request for Firefox.
|
|
167
|
-
def normalize_reporting_endpoint
|
|
168
|
-
if @ua && @ua =~ /Firefox/
|
|
169
|
-
if same_origin? || report_uri.nil? || URI.parse(report_uri).host.nil?
|
|
170
|
-
return
|
|
171
|
-
end
|
|
172
|
-
|
|
173
|
-
if forward_endpoint
|
|
174
|
-
@report_uri = FF_CSP_ENDPOINT
|
|
175
|
-
end
|
|
176
|
-
end
|
|
177
|
-
|
|
178
|
-
if @tag_report_uri
|
|
179
|
-
@report_uri = "#{@report_uri}?enforce=#{@enforce}"
|
|
180
|
-
@report_uri += "&app_name=#{@app_name}" if @app_name
|
|
181
|
-
end
|
|
182
|
-
end
|
|
183
|
-
|
|
184
|
-
def same_origin?
|
|
185
|
-
return unless report_uri && request_uri
|
|
186
|
-
|
|
187
|
-
begin
|
|
188
|
-
origin = URI.parse(request_uri)
|
|
189
|
-
uri = URI.parse(report_uri)
|
|
190
|
-
rescue URI::InvalidURIError
|
|
191
|
-
return false
|
|
192
|
-
end
|
|
193
|
-
|
|
194
|
-
uri.host == origin.host && origin.port == uri.port && origin.scheme == uri.scheme
|
|
195
|
-
end
|
|
196
|
-
|
|
197
209
|
def report_uri_directive
|
|
198
210
|
return '' if @report_uri.nil?
|
|
199
211
|
|
|
@@ -205,44 +217,40 @@ module SecureHeaders
|
|
|
205
217
|
end
|
|
206
218
|
end
|
|
207
219
|
|
|
220
|
+
if @tag_report_uri
|
|
221
|
+
@report_uri = "#{@report_uri}?enforce=#{@enforce}"
|
|
222
|
+
@report_uri += "&app_name=#{@app_name}" if @app_name
|
|
223
|
+
end
|
|
224
|
+
|
|
208
225
|
"report-uri #{@report_uri};"
|
|
209
226
|
end
|
|
210
227
|
|
|
211
|
-
def generic_directives
|
|
228
|
+
def generic_directives
|
|
212
229
|
header_value = ''
|
|
213
|
-
if config[:img_src]
|
|
214
|
-
config[:img_src] = config[:img_src] + ['data:'] unless config[:img_src].include?('data:')
|
|
230
|
+
if @config[:img_src]
|
|
231
|
+
@config[:img_src] = @config[:img_src] + ['data:'] unless @config[:img_src].include?('data:')
|
|
215
232
|
else
|
|
216
|
-
config[:img_src] = config[:default_src] + ['data:']
|
|
233
|
+
@config[:img_src] = @config[:default_src] + ['data:']
|
|
217
234
|
end
|
|
218
235
|
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
header_value += build_directive(k)
|
|
236
|
+
DIRECTIVES.each do |directive_name|
|
|
237
|
+
header_value += build_directive(directive_name) if @config[directive_name]
|
|
222
238
|
end
|
|
223
239
|
|
|
224
240
|
header_value
|
|
225
241
|
end
|
|
226
242
|
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
243
|
+
def non_default_directives
|
|
244
|
+
header_value = ''
|
|
245
|
+
NON_DEFAULT_SOURCES.each do |directive_name|
|
|
246
|
+
header_value += build_directive(directive_name) if @config[directive_name]
|
|
247
|
+
end
|
|
231
248
|
|
|
232
|
-
|
|
233
|
-
sym.to_s.gsub('_', '-')
|
|
249
|
+
header_value
|
|
234
250
|
end
|
|
235
251
|
|
|
236
|
-
def
|
|
237
|
-
@
|
|
238
|
-
@ua = request.env['HTTP_USER_AGENT']
|
|
239
|
-
@request_uri = if request.respond_to?(:original_url)
|
|
240
|
-
# rails 3.1+
|
|
241
|
-
request.original_url
|
|
242
|
-
else
|
|
243
|
-
# rails 2/3.0
|
|
244
|
-
request.url
|
|
245
|
-
end
|
|
252
|
+
def build_directive(key)
|
|
253
|
+
"#{self.class.symbol_to_hyphen_case(key)} #{@config[key].join(" ")}; "
|
|
246
254
|
end
|
|
247
255
|
end
|
|
248
256
|
end
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
module SecureHeaders
|
|
2
|
+
class XDOBuildError < StandardError; end
|
|
3
|
+
class XDownloadOptions < Header
|
|
4
|
+
module Constants
|
|
5
|
+
XDO_HEADER_NAME = "X-Download-Options"
|
|
6
|
+
DEFAULT_VALUE = 'noopen'
|
|
7
|
+
end
|
|
8
|
+
include Constants
|
|
9
|
+
|
|
10
|
+
def initialize(config = nil)
|
|
11
|
+
@config = config
|
|
12
|
+
validate_config unless @config.nil?
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def name
|
|
16
|
+
XDO_HEADER_NAME
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def value
|
|
20
|
+
case @config
|
|
21
|
+
when NilClass
|
|
22
|
+
DEFAULT_VALUE
|
|
23
|
+
when String
|
|
24
|
+
@config
|
|
25
|
+
else
|
|
26
|
+
@config[:value]
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
private
|
|
31
|
+
|
|
32
|
+
def validate_config
|
|
33
|
+
value = @config.is_a?(Hash) ? @config[:value] : @config
|
|
34
|
+
unless value.casecmp(DEFAULT_VALUE) == 0
|
|
35
|
+
raise XDOBuildError.new("Value can only be nil or 'noopen'")
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|