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.
Files changed (113) hide show
  1. data/.gitignore +4 -8
  2. data/.travis.yml +7 -4
  3. data/Gemfile +6 -7
  4. data/README.md +115 -79
  5. data/Rakefile +11 -116
  6. data/fixtures/rails_3_2_12/Gemfile +0 -6
  7. data/fixtures/rails_3_2_12/app/controllers/things_controller.rb +0 -1
  8. data/fixtures/rails_3_2_12/app/views/layouts/application.html.erb +1 -4
  9. data/fixtures/rails_3_2_12/app/views/other_things/index.html.erb +2 -1
  10. data/fixtures/rails_3_2_12/app/views/things/index.html.erb +1 -21
  11. data/fixtures/rails_3_2_12/config/application.rb +0 -54
  12. data/fixtures/rails_3_2_12/config/environments/test.rb +2 -2
  13. data/fixtures/rails_3_2_12/config/initializers/secure_headers.rb +2 -1
  14. data/fixtures/rails_3_2_12/config/routes.rb +0 -57
  15. data/fixtures/rails_3_2_12/config/script_hashes.yml +5 -0
  16. data/fixtures/rails_3_2_12/config.ru +3 -0
  17. data/fixtures/rails_3_2_12/spec/controllers/other_things_controller_spec.rb +58 -16
  18. data/fixtures/rails_3_2_12/spec/controllers/things_controller_spec.rb +11 -1
  19. data/fixtures/rails_3_2_12_no_init/Gemfile +0 -7
  20. data/fixtures/rails_3_2_12_no_init/app/controllers/other_things_controller.rb +1 -2
  21. data/fixtures/rails_3_2_12_no_init/app/controllers/things_controller.rb +1 -1
  22. data/fixtures/rails_3_2_12_no_init/app/views/layouts/application.html.erb +0 -2
  23. data/fixtures/rails_3_2_12_no_init/app/views/things/index.html.erb +0 -21
  24. data/fixtures/rails_3_2_12_no_init/config/application.rb +0 -51
  25. data/fixtures/rails_3_2_12_no_init/config/environments/test.rb +2 -2
  26. data/fixtures/rails_3_2_12_no_init/config/routes.rb +0 -57
  27. data/fixtures/rails_3_2_12_no_init/spec/controllers/other_things_controller_spec.rb +10 -0
  28. data/fixtures/rails_3_2_12_no_init/spec/controllers/things_controller_spec.rb +10 -0
  29. data/fixtures/rails_4_1_8/Gemfile +5 -0
  30. data/fixtures/rails_4_1_8/README.rdoc +28 -0
  31. data/fixtures/rails_4_1_8/Rakefile +6 -0
  32. data/fixtures/rails_4_1_8/app/controllers/application_controller.rb +4 -0
  33. data/fixtures/rails_4_1_8/app/controllers/concerns/.keep +0 -0
  34. data/fixtures/rails_4_1_8/app/controllers/other_things_controller.rb +5 -0
  35. data/fixtures/rails_4_1_8/app/controllers/things_controller.rb +5 -0
  36. data/fixtures/rails_4_1_8/app/models/.keep +0 -0
  37. data/fixtures/rails_4_1_8/app/models/concerns/.keep +0 -0
  38. data/fixtures/rails_4_1_8/app/views/layouts/application.html.erb +11 -0
  39. data/fixtures/rails_4_1_8/app/views/other_things/index.html.erb +2 -0
  40. data/fixtures/rails_4_1_8/app/views/things/index.html.erb +1 -0
  41. data/fixtures/rails_4_1_8/config/application.rb +15 -0
  42. data/fixtures/rails_4_1_8/config/boot.rb +4 -0
  43. data/fixtures/rails_4_1_8/config/environment.rb +5 -0
  44. data/fixtures/rails_4_1_8/config/environments/test.rb +10 -0
  45. data/fixtures/rails_4_1_8/config/initializers/secure_headers.rb +17 -0
  46. data/fixtures/rails_4_1_8/config/routes.rb +4 -0
  47. data/fixtures/rails_4_1_8/config/script_hashes.yml +5 -0
  48. data/fixtures/rails_4_1_8/config/secrets.yml +22 -0
  49. data/fixtures/rails_4_1_8/config.ru +4 -0
  50. data/fixtures/rails_4_1_8/lib/assets/.keep +0 -0
  51. data/fixtures/rails_4_1_8/lib/tasks/.keep +0 -0
  52. data/fixtures/rails_4_1_8/log/.keep +0 -0
  53. data/fixtures/rails_4_1_8/spec/controllers/other_things_controller_spec.rb +83 -0
  54. data/fixtures/rails_4_1_8/spec/controllers/things_controller_spec.rb +59 -0
  55. data/fixtures/rails_4_1_8/spec/spec_helper.rb +15 -0
  56. data/fixtures/rails_4_1_8/vendor/assets/javascripts/.keep +0 -0
  57. data/fixtures/rails_4_1_8/vendor/assets/stylesheets/.keep +0 -0
  58. data/lib/secure_headers/hash_helper.rb +7 -0
  59. data/lib/secure_headers/headers/content_security_policy/script_hash_middleware.rb +22 -0
  60. data/lib/secure_headers/headers/content_security_policy.rb +141 -133
  61. data/lib/secure_headers/headers/x_download_options.rb +39 -0
  62. data/lib/secure_headers/headers/x_permitted_cross_domain_policies.rb +40 -0
  63. data/lib/secure_headers/headers/x_xss_protection.rb +2 -1
  64. data/lib/secure_headers/railtie.rb +19 -24
  65. data/lib/secure_headers/version.rb +1 -1
  66. data/lib/secure_headers/view_helper.rb +68 -0
  67. data/lib/secure_headers.rb +65 -16
  68. data/lib/tasks/tasks.rake +48 -0
  69. data/secure_headers.gemspec +4 -4
  70. data/spec/lib/secure_headers/headers/content_security_policy/script_hash_middleware_spec.rb +47 -0
  71. data/spec/lib/secure_headers/headers/content_security_policy_spec.rb +83 -208
  72. data/spec/lib/secure_headers/headers/x_download_options_spec.rb +32 -0
  73. data/spec/lib/secure_headers/headers/x_permitted_cross_domain_policies_spec.rb +64 -0
  74. data/spec/lib/secure_headers/headers/x_xss_protection_spec.rb +2 -1
  75. data/spec/lib/secure_headers_spec.rb +57 -61
  76. data/spec/spec_helper.rb +27 -1
  77. metadata +47 -42
  78. data/Guardfile +0 -6
  79. data/HISTORY.md +0 -155
  80. data/app/controllers/content_security_policy_controller.rb +0 -75
  81. data/config/curl-ca-bundle.crt +0 -5420
  82. data/config/routes.rb +0 -3
  83. data/fixtures/rails_3_2_12/app/models/thing.rb +0 -3
  84. data/fixtures/rails_3_2_12/config/database.yml +0 -25
  85. data/fixtures/rails_3_2_12/config/environments/development.rb +0 -37
  86. data/fixtures/rails_3_2_12/config/environments/production.rb +0 -67
  87. data/fixtures/rails_3_2_12/config/initializers/backtrace_silencers.rb +0 -7
  88. data/fixtures/rails_3_2_12/config/initializers/inflections.rb +0 -15
  89. data/fixtures/rails_3_2_12/config/initializers/mime_types.rb +0 -5
  90. data/fixtures/rails_3_2_12/config/initializers/secret_token.rb +0 -7
  91. data/fixtures/rails_3_2_12/config/initializers/session_store.rb +0 -8
  92. data/fixtures/rails_3_2_12/config/initializers/wrap_parameters.rb +0 -14
  93. data/fixtures/rails_3_2_12/config/locales/en.yml +0 -5
  94. data/fixtures/rails_3_2_12/db/schema.rb +0 -16
  95. data/fixtures/rails_3_2_12/db/seeds.rb +0 -7
  96. data/fixtures/rails_3_2_12_no_init/app/models/thing.rb +0 -3
  97. data/fixtures/rails_3_2_12_no_init/app/views/things/_form.html.erb +0 -17
  98. data/fixtures/rails_3_2_12_no_init/app/views/things/edit.html.erb +0 -6
  99. data/fixtures/rails_3_2_12_no_init/app/views/things/new.html.erb +0 -5
  100. data/fixtures/rails_3_2_12_no_init/app/views/things/show.html.erb +0 -5
  101. data/fixtures/rails_3_2_12_no_init/config/database.yml +0 -25
  102. data/fixtures/rails_3_2_12_no_init/config/environments/development.rb +0 -37
  103. data/fixtures/rails_3_2_12_no_init/config/environments/production.rb +0 -67
  104. data/fixtures/rails_3_2_12_no_init/config/initializers/backtrace_silencers.rb +0 -7
  105. data/fixtures/rails_3_2_12_no_init/config/initializers/inflections.rb +0 -15
  106. data/fixtures/rails_3_2_12_no_init/config/initializers/mime_types.rb +0 -5
  107. data/fixtures/rails_3_2_12_no_init/config/initializers/secret_token.rb +0 -7
  108. data/fixtures/rails_3_2_12_no_init/config/initializers/session_store.rb +0 -8
  109. data/fixtures/rails_3_2_12_no_init/config/initializers/wrap_parameters.rb +0 -14
  110. data/fixtures/rails_3_2_12_no_init/config/locales/en.yml +0 -5
  111. data/fixtures/rails_3_2_12_no_init/db/schema.rb +0 -16
  112. data/fixtures/rails_3_2_12_no_init/db/seeds.rb +0 -7
  113. 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"] %>
@@ -0,0 +1,4 @@
1
+ # This file is used by Rack-based servers to start the application.
2
+
3
+ require ::File.expand_path('../config/environment', __FILE__)
4
+ run Rails.application
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
+
@@ -0,0 +1,7 @@
1
+ module SecureHeaders
2
+ module HashHelper
3
+ def hash_source(inline_script, digest = :SHA256)
4
+ [digest.to_s.downcase, "-", [[Digest.const_get(digest).hexdigest(inline_script)].pack("H*")].pack("m").chomp].join
5
+ end
6
+ end
7
+ end
@@ -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://* about: javascript:; img-src data:"
9
- STANDARD_HEADER_NAME = "Content-Security-Policy"
10
- FF_CSP_ENDPOINT = "/content_security_policy/forward_report"
11
- DIRECTIVES = [:default_src, :script_src, :frame_src, :style_src, :img_src, :media_src, :font_src, :object_src, :connect_src]
12
- META = [:disable_chrome_extension, :disable_fill_missing, :forward_endpoint]
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
- attr_accessor *META
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
- # :experimental use experimental block for config
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
- @experimental = !!options.delete(:experimental)
33
- @controller = options.delete(:controller)
90
+ return unless config
34
91
 
35
92
  if options[:request]
36
- parse_request(options[:request])
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
- configure(config) if config
48
- end
96
+ @controller = options[:controller]
97
+ @ua = options[:ua]
98
+ @ssl_request = !!options.delete(:ssl)
99
+ @request_uri = options.delete(:request_uri)
49
100
 
50
- def nonce
51
- @nonce ||= SecureRandom.base64(32).chomp
52
- end
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
- def configure(config)
55
- @config = config.dup
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
- experimental_config = @config.delete(:experimental)
58
- if @experimental && experimental_config
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
- normalize_csp_options
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 = @config.delete(:tag_report_uri)
124
+ @tag_report_uri = !!@config.delete(:tag_report_uri)
125
+ @script_hashes = @config.delete(:script_hashes) || []
75
126
 
76
- normalize_reporting_endpoint
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 = STANDARD_HEADER_NAME
82
- if !@enforce || experimental
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(@config),
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
- return unless @config[:default_src]
112
- default = @config[:default_src]
113
- DIRECTIVES.each do |directive|
114
- unless @config[directive]
115
- @config[directive] = default
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 in chrome
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.instance_variable_set(:@content_security_policy_nonce, nonce)
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(config)
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
- header_value = build_directive(:default_src)
220
- config.keys.sort_by{|k| k.to_s}.each do |k| # ensure consistent ordering
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
- # build and deletes the directive
228
- def build_directive(key)
229
- "#{symbol_to_hyphen_case(key)} #{@config.delete(key).join(" ")}; "
230
- end
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
- def symbol_to_hyphen_case sym
233
- sym.to_s.gsub('_', '-')
249
+ header_value
234
250
  end
235
251
 
236
- def parse_request request
237
- @ssl_request = request.ssl?
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