secure_headers 2.2.0 → 3.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 (127) hide show
  1. checksums.yaml +4 -4
  2. data/.rspec +1 -0
  3. data/.ruby-version +1 -1
  4. data/.travis.yml +3 -1
  5. data/CHANGELOG.md +205 -0
  6. data/Gemfile +9 -10
  7. data/Guardfile +12 -0
  8. data/README.md +175 -276
  9. data/Rakefile +2 -36
  10. data/lib/secure_headers/configuration.rb +189 -0
  11. data/lib/secure_headers/headers/content_security_policy.rb +367 -188
  12. data/lib/secure_headers/headers/public_key_pins.rb +43 -57
  13. data/lib/secure_headers/headers/strict_transport_security.rb +21 -47
  14. data/lib/secure_headers/headers/x_content_type_options.rb +19 -32
  15. data/lib/secure_headers/headers/x_download_options.rb +18 -31
  16. data/lib/secure_headers/headers/x_frame_options.rb +24 -32
  17. data/lib/secure_headers/headers/x_permitted_cross_domain_policies.rb +19 -32
  18. data/lib/secure_headers/headers/x_xss_protection.rb +17 -46
  19. data/lib/secure_headers/middleware.rb +15 -0
  20. data/lib/secure_headers/padrino.rb +1 -2
  21. data/lib/secure_headers/railtie.rb +9 -6
  22. data/lib/secure_headers/view_helper.rb +27 -44
  23. data/lib/secure_headers.rb +244 -145
  24. data/secure_headers.gemspec +7 -11
  25. data/spec/lib/secure_headers/configuration_spec.rb +80 -0
  26. data/spec/lib/secure_headers/headers/content_security_policy_spec.rb +176 -213
  27. data/spec/lib/secure_headers/headers/public_key_pins_spec.rb +17 -17
  28. data/spec/lib/secure_headers/headers/strict_transport_security_spec.rb +11 -43
  29. data/spec/lib/secure_headers/headers/x_content_type_options_spec.rb +13 -18
  30. data/spec/lib/secure_headers/headers/x_download_options_spec.rb +13 -17
  31. data/spec/lib/secure_headers/headers/x_frame_options_spec.rb +15 -17
  32. data/spec/lib/secure_headers/headers/x_permitted_cross_domain_policies_spec.rb +22 -39
  33. data/spec/lib/secure_headers/headers/x_xss_protection_spec.rb +22 -30
  34. data/spec/lib/secure_headers/middleware_spec.rb +40 -0
  35. data/spec/lib/secure_headers_spec.rb +204 -278
  36. data/spec/spec_helper.rb +32 -25
  37. data/upgrading-to-3-0.md +37 -0
  38. metadata +28 -95
  39. data/fixtures/rails_3_2_12/.rspec +0 -1
  40. data/fixtures/rails_3_2_12/Gemfile +0 -6
  41. data/fixtures/rails_3_2_12/README.rdoc +0 -261
  42. data/fixtures/rails_3_2_12/Rakefile +0 -7
  43. data/fixtures/rails_3_2_12/app/controllers/application_controller.rb +0 -4
  44. data/fixtures/rails_3_2_12/app/controllers/other_things_controller.rb +0 -5
  45. data/fixtures/rails_3_2_12/app/controllers/things_controller.rb +0 -5
  46. data/fixtures/rails_3_2_12/app/models/.gitkeep +0 -0
  47. data/fixtures/rails_3_2_12/app/views/layouts/application.html.erb +0 -11
  48. data/fixtures/rails_3_2_12/app/views/other_things/index.html.erb +0 -2
  49. data/fixtures/rails_3_2_12/app/views/things/index.html.erb +0 -1
  50. data/fixtures/rails_3_2_12/config/application.rb +0 -14
  51. data/fixtures/rails_3_2_12/config/boot.rb +0 -6
  52. data/fixtures/rails_3_2_12/config/environment.rb +0 -5
  53. data/fixtures/rails_3_2_12/config/environments/test.rb +0 -37
  54. data/fixtures/rails_3_2_12/config/initializers/secure_headers.rb +0 -17
  55. data/fixtures/rails_3_2_12/config/routes.rb +0 -4
  56. data/fixtures/rails_3_2_12/config/script_hashes.yml +0 -5
  57. data/fixtures/rails_3_2_12/config.ru +0 -7
  58. data/fixtures/rails_3_2_12/lib/assets/.gitkeep +0 -0
  59. data/fixtures/rails_3_2_12/lib/tasks/.gitkeep +0 -0
  60. data/fixtures/rails_3_2_12/log/.gitkeep +0 -0
  61. data/fixtures/rails_3_2_12/spec/controllers/other_things_controller_spec.rb +0 -83
  62. data/fixtures/rails_3_2_12/spec/controllers/things_controller_spec.rb +0 -54
  63. data/fixtures/rails_3_2_12/spec/spec_helper.rb +0 -15
  64. data/fixtures/rails_3_2_12/vendor/assets/javascripts/.gitkeep +0 -0
  65. data/fixtures/rails_3_2_12/vendor/assets/stylesheets/.gitkeep +0 -0
  66. data/fixtures/rails_3_2_12/vendor/plugins/.gitkeep +0 -0
  67. data/fixtures/rails_3_2_12_no_init/.rspec +0 -1
  68. data/fixtures/rails_3_2_12_no_init/Gemfile +0 -5
  69. data/fixtures/rails_3_2_12_no_init/README.rdoc +0 -261
  70. data/fixtures/rails_3_2_12_no_init/Rakefile +0 -7
  71. data/fixtures/rails_3_2_12_no_init/app/controllers/application_controller.rb +0 -4
  72. data/fixtures/rails_3_2_12_no_init/app/controllers/other_things_controller.rb +0 -6
  73. data/fixtures/rails_3_2_12_no_init/app/controllers/things_controller.rb +0 -5
  74. data/fixtures/rails_3_2_12_no_init/app/models/.gitkeep +0 -0
  75. data/fixtures/rails_3_2_12_no_init/app/views/layouts/application.html.erb +0 -12
  76. data/fixtures/rails_3_2_12_no_init/app/views/other_things/index.html.erb +0 -1
  77. data/fixtures/rails_3_2_12_no_init/app/views/things/index.html.erb +0 -0
  78. data/fixtures/rails_3_2_12_no_init/config/application.rb +0 -17
  79. data/fixtures/rails_3_2_12_no_init/config/boot.rb +0 -6
  80. data/fixtures/rails_3_2_12_no_init/config/environment.rb +0 -5
  81. data/fixtures/rails_3_2_12_no_init/config/environments/test.rb +0 -37
  82. data/fixtures/rails_3_2_12_no_init/config/routes.rb +0 -4
  83. data/fixtures/rails_3_2_12_no_init/config.ru +0 -4
  84. data/fixtures/rails_3_2_12_no_init/lib/assets/.gitkeep +0 -0
  85. data/fixtures/rails_3_2_12_no_init/lib/tasks/.gitkeep +0 -0
  86. data/fixtures/rails_3_2_12_no_init/log/.gitkeep +0 -0
  87. data/fixtures/rails_3_2_12_no_init/spec/controllers/other_things_controller_spec.rb +0 -50
  88. data/fixtures/rails_3_2_12_no_init/spec/controllers/things_controller_spec.rb +0 -54
  89. data/fixtures/rails_3_2_12_no_init/spec/spec_helper.rb +0 -5
  90. data/fixtures/rails_3_2_12_no_init/vendor/assets/javascripts/.gitkeep +0 -0
  91. data/fixtures/rails_3_2_12_no_init/vendor/assets/stylesheets/.gitkeep +0 -0
  92. data/fixtures/rails_3_2_12_no_init/vendor/plugins/.gitkeep +0 -0
  93. data/fixtures/rails_4_1_8/Gemfile +0 -5
  94. data/fixtures/rails_4_1_8/README.rdoc +0 -28
  95. data/fixtures/rails_4_1_8/Rakefile +0 -6
  96. data/fixtures/rails_4_1_8/app/controllers/application_controller.rb +0 -4
  97. data/fixtures/rails_4_1_8/app/controllers/concerns/.keep +0 -0
  98. data/fixtures/rails_4_1_8/app/controllers/other_things_controller.rb +0 -5
  99. data/fixtures/rails_4_1_8/app/controllers/things_controller.rb +0 -5
  100. data/fixtures/rails_4_1_8/app/models/.keep +0 -0
  101. data/fixtures/rails_4_1_8/app/models/concerns/.keep +0 -0
  102. data/fixtures/rails_4_1_8/app/views/layouts/application.html.erb +0 -11
  103. data/fixtures/rails_4_1_8/app/views/other_things/index.html.erb +0 -2
  104. data/fixtures/rails_4_1_8/app/views/things/index.html.erb +0 -1
  105. data/fixtures/rails_4_1_8/config/application.rb +0 -15
  106. data/fixtures/rails_4_1_8/config/boot.rb +0 -4
  107. data/fixtures/rails_4_1_8/config/environment.rb +0 -5
  108. data/fixtures/rails_4_1_8/config/environments/test.rb +0 -10
  109. data/fixtures/rails_4_1_8/config/initializers/secure_headers.rb +0 -17
  110. data/fixtures/rails_4_1_8/config/routes.rb +0 -4
  111. data/fixtures/rails_4_1_8/config/script_hashes.yml +0 -5
  112. data/fixtures/rails_4_1_8/config/secrets.yml +0 -22
  113. data/fixtures/rails_4_1_8/config.ru +0 -4
  114. data/fixtures/rails_4_1_8/lib/assets/.keep +0 -0
  115. data/fixtures/rails_4_1_8/lib/tasks/.keep +0 -0
  116. data/fixtures/rails_4_1_8/log/.keep +0 -0
  117. data/fixtures/rails_4_1_8/spec/controllers/other_things_controller_spec.rb +0 -83
  118. data/fixtures/rails_4_1_8/spec/controllers/things_controller_spec.rb +0 -59
  119. data/fixtures/rails_4_1_8/spec/spec_helper.rb +0 -15
  120. data/fixtures/rails_4_1_8/vendor/assets/javascripts/.keep +0 -0
  121. data/fixtures/rails_4_1_8/vendor/assets/stylesheets/.keep +0 -0
  122. data/lib/secure_headers/hash_helper.rb +0 -7
  123. data/lib/secure_headers/header.rb +0 -5
  124. data/lib/secure_headers/headers/content_security_policy/script_hash_middleware.rb +0 -22
  125. data/lib/secure_headers/version.rb +0 -3
  126. data/lib/tasks/tasks.rake +0 -48
  127. data/spec/lib/secure_headers/headers/content_security_policy/script_hash_middleware_spec.rb +0 -47
@@ -1,62 +1,45 @@
1
1
  module SecureHeaders
2
- class UnexpectedHashedScriptException < StandardError
3
-
4
- end
5
-
6
2
  module ViewHelpers
7
- include SecureHeaders::HashHelper
8
- SECURE_HEADERS_RAKE_TASK = "rake secure_headers:generate_hashes"
9
-
10
- def nonced_style_tag(content = nil, &block)
11
- nonced_tag(content, :style, block)
3
+ # Public: create a style tag using the content security policy nonce.
4
+ # Instructs secure_headers to append a nonce to style/script-src directives.
5
+ #
6
+ # Returns an html-safe style tag with the nonce attribute.
7
+ def nonced_style_tag(content_or_options = {}, &block)
8
+ nonced_tag(:style, content_or_options, block)
12
9
  end
13
10
 
14
- def nonced_javascript_tag(content = nil, &block)
15
- nonced_tag(content, :script, block)
11
+ # Public: create a script tag using the content security policy nonce.
12
+ # Instructs secure_headers to append a nonce to style/script-src directives.
13
+ #
14
+ # Returns an html-safe script tag with the nonce attribute.
15
+ def nonced_javascript_tag(content_or_options = {}, &block)
16
+ nonced_tag(:script, content_or_options, block)
16
17
  end
17
18
 
18
- def hashed_javascript_tag(raise_error_on_unrecognized_hash = false, &block)
19
- content = capture(&block)
20
-
21
- if ['development', 'test'].include?(ENV["RAILS_ENV"])
22
- hash_value = hash_source(content)
23
- file_path = File.join('app', 'views', self.instance_variable_get(:@virtual_path) + '.html.erb')
24
- script_hashes = controller.instance_variable_get(:@script_hashes)[file_path]
25
- unless script_hashes && script_hashes.include?(hash_value)
26
- message = unexpected_hash_error_message(file_path, hash_value, content)
27
- if raise_error_on_unrecognized_hash
28
- raise UnexpectedHashedScriptException.new(message)
29
- else
30
- puts message
31
- request.env[HASHES_ENV_KEY] = (request.env[HASHES_ENV_KEY] || []) << hash_value
32
- end
33
- end
19
+ # Public: use the content security policy nonce for this request directly.
20
+ # Instructs secure_headers to append a nonce to style/script-src directives.
21
+ #
22
+ # Returns a non-html-safe nonce value.
23
+ def content_security_policy_nonce(type)
24
+ case type
25
+ when :script
26
+ SecureHeaders.content_security_policy_script_nonce(@_request)
27
+ when :style
28
+ SecureHeaders.content_security_policy_style_nonce(@_request)
34
29
  end
35
-
36
- content_tag :script, content
37
30
  end
38
31
 
39
32
  private
40
33
 
41
- def nonced_tag(content, type, block)
34
+ def nonced_tag(type, content_or_options, block)
35
+ options = {}
42
36
  content = if block
37
+ options = content_or_options
43
38
  capture(&block)
44
39
  else
45
- content.html_safe # :'(
40
+ content_or_options.html_safe # :'(
46
41
  end
47
-
48
- content_tag type, content, :nonce => @content_security_policy_nonce
49
- end
50
-
51
- def unexpected_hash_error_message(file_path, hash_value, content)
52
- <<-EOF
53
- \n\n*** WARNING: Unrecognized hash in #{file_path}!!! Value: #{hash_value} ***
54
- <script>#{content}</script>
55
- *** This is fine in dev/test, but will raise exceptions in production. ***
56
- *** Run #{SECURE_HEADERS_RAKE_TASK} or add the following to config/script_hashes.yml:***
57
- #{file_path}:
58
- - #{hash_value}\n\n
59
- EOF
42
+ content_tag type, content, options.merge(nonce: content_security_policy_nonce(type))
60
43
  end
61
44
  end
62
45
  end
@@ -1,193 +1,292 @@
1
+ require "secure_headers/configuration"
2
+ require "secure_headers/headers/public_key_pins"
3
+ require "secure_headers/headers/content_security_policy"
4
+ require "secure_headers/headers/x_frame_options"
5
+ require "secure_headers/headers/strict_transport_security"
6
+ require "secure_headers/headers/x_xss_protection"
7
+ require "secure_headers/headers/x_content_type_options"
8
+ require "secure_headers/headers/x_download_options"
9
+ require "secure_headers/headers/x_permitted_cross_domain_policies"
10
+ require "secure_headers/middleware"
11
+ require "secure_headers/railtie"
12
+ require "secure_headers/view_helper"
13
+ require "useragent"
14
+
15
+ # All headers (except for hpkp) have a default value. Provide SecureHeaders::OPT_OUT
16
+ # or ":optout_of_protection" as a config value to disable a given header
1
17
  module SecureHeaders
2
- SCRIPT_HASH_CONFIG_FILE = 'config/script_hashes.yml'
3
- HASHES_ENV_KEY = 'secure_headers.script_hashes'
4
-
5
- module Configuration
6
- class << self
7
- attr_accessor :hsts, :x_frame_options, :x_content_type_options,
8
- :x_xss_protection, :csp, :x_download_options, :script_hashes,
9
- :x_permitted_cross_domain_policies, :hpkp
10
-
11
- def configure &block
12
- instance_eval &block
13
- if File.exists?(SCRIPT_HASH_CONFIG_FILE)
14
- ::SecureHeaders::Configuration.script_hashes = YAML.load(File.open(SCRIPT_HASH_CONFIG_FILE))
18
+ OPT_OUT = :opt_out_of_protection
19
+ SECURE_HEADERS_CONFIG = "secure_headers_request_config".freeze
20
+ NONCE_KEY = "secure_headers_content_security_policy_nonce".freeze
21
+ HTTPS = "https".freeze
22
+ CSP = ContentSecurityPolicy
23
+
24
+ ALL_HEADER_CLASSES = [
25
+ ContentSecurityPolicy,
26
+ StrictTransportSecurity,
27
+ PublicKeyPins,
28
+ XContentTypeOptions,
29
+ XDownloadOptions,
30
+ XFrameOptions,
31
+ XPermittedCrossDomainPolicies,
32
+ XXssProtection
33
+ ].freeze
34
+
35
+ ALL_HEADERS_BESIDES_CSP = (ALL_HEADER_CLASSES - [CSP]).freeze
36
+
37
+ # Headers set on http requests (excludes STS and HPKP)
38
+ HTTP_HEADER_CLASSES =
39
+ (ALL_HEADER_CLASSES - [StrictTransportSecurity, PublicKeyPins]).freeze
40
+
41
+ class << self
42
+ # Public: override a given set of directives for the current request. If a
43
+ # value already exists for a given directive, it will be overridden.
44
+ #
45
+ # If CSP was previously OPT_OUT, a new blank policy is used.
46
+ #
47
+ # additions - a hash containing directives. e.g.
48
+ # script_src: %w(another-host.com)
49
+ def override_content_security_policy_directives(request, additions)
50
+ config = config_for(request)
51
+ unless CSP.idempotent_additions?(config.csp, additions)
52
+ config = config.dup
53
+ if config.csp == OPT_OUT
54
+ config.csp = {}
15
55
  end
56
+ config.csp.merge!(additions)
57
+ override_secure_headers_request_config(request, config)
16
58
  end
17
59
  end
18
- end
19
60
 
20
- class << self
21
- def append_features(base)
22
- base.module_eval do
23
- extend ClassMethods
24
- include InstanceMethods
61
+ # Public: appends source values to the current configuration. If no value
62
+ # is set for a given directive, the value will be merged with the default-src
63
+ # value. If a value exists for the given directive, the values will be combined.
64
+ #
65
+ # additions - a hash containing directives. e.g.
66
+ # script_src: %w(another-host.com)
67
+ def append_content_security_policy_directives(request, additions)
68
+ config = config_for(request)
69
+ unless CSP.idempotent_additions?(config.csp, additions)
70
+ config = config.dup
71
+ config.csp = CSP.combine_policies(config.csp, additions)
72
+ override_secure_headers_request_config(request, config)
25
73
  end
26
74
  end
27
- end
28
75
 
29
- module ClassMethods
30
- attr_writer :secure_headers_options
31
- def secure_headers_options
32
- if @secure_headers_options
33
- @secure_headers_options
34
- elsif superclass.respond_to?(:secure_headers_options) # stop at application_controller
35
- superclass.secure_headers_options
36
- else
37
- {}
38
- end
76
+ # Public: override X-Frame-Options settings for this request.
77
+ #
78
+ # value - deny, sameorigin, or allowall
79
+ #
80
+ # Returns the current config
81
+ def override_x_frame_options(request, value)
82
+ default_config = config_for(request).dup
83
+ default_config.x_frame_options = value
84
+ override_secure_headers_request_config(request, default_config)
39
85
  end
40
86
 
41
- def ensure_security_headers options = {}
42
- self.secure_headers_options = options
43
- before_filter :prep_script_hash
44
- before_filter :set_hsts_header
45
- before_filter :set_hpkp_header
46
- before_filter :set_x_frame_options_header
47
- before_filter :set_csp_header
48
- before_filter :set_x_xss_protection_header
49
- before_filter :set_x_content_type_options_header
50
- before_filter :set_x_download_options_header
51
- before_filter :set_x_permitted_cross_domain_policies_header
87
+ # Public: opts out of setting a given header by creating a temporary config
88
+ # and setting the given headers config to OPT_OUT.
89
+ def opt_out_of_header(request, header_key)
90
+ config = config_for(request).dup
91
+ config.send("#{header_key}=", OPT_OUT)
92
+ override_secure_headers_request_config(request, config)
52
93
  end
53
94
 
54
- # we can't use ||= because I'm overloading false => disable, nil => default
55
- # both of which trigger the conditional assignment
56
- def options_for(type, options)
57
- options.nil? ? ::SecureHeaders::Configuration.send(type) : options
95
+ # Public: opts out of setting all headers by telling secure_headers to use
96
+ # the NOOP configuration.
97
+ def opt_out_of_all_protection(request)
98
+ use_secure_headers_override(request, Configuration::NOOP_CONFIGURATION)
58
99
  end
59
- end
60
100
 
61
- module InstanceMethods
62
- def set_security_headers(options = self.class.secure_headers_options)
63
- set_csp_header(request, options[:csp])
64
- set_hsts_header(options[:hsts])
65
- set_hpkp_header(options[:hpkp])
66
- set_x_frame_options_header(options[:x_frame_options])
67
- set_x_xss_protection_header(options[:x_xss_protection])
68
- set_x_content_type_options_header(options[:x_content_type_options])
69
- set_x_download_options_header(options[:x_download_options])
70
- set_x_permitted_cross_domain_policies_header(options[:x_permitted_cross_domain_policies])
71
- end
72
-
73
- # set_csp_header - uses the request accessor and SecureHeader::Configuration settings
74
- # set_csp_header(+Rack::Request+) - uses the parameter and and SecureHeader::Configuration settings
75
- # set_csp_header(+Hash+) - uses the request accessor and options from parameters
76
- # set_csp_header(+Rack::Request+, +Hash+)
77
- def set_csp_header(req = nil, config=nil)
78
- if req.is_a?(Hash) || req.is_a?(FalseClass)
79
- config = req
101
+ # Public: Builds the hash of headers that should be applied base on the
102
+ # request.
103
+ #
104
+ # StrictTransportSecurity and PublicKeyPins are not applied to http requests.
105
+ # See #config_for to determine which config is used for a given request.
106
+ #
107
+ # Returns a hash of header names => header values. The value
108
+ # returned is meant to be merged into the header value from `@app.call(env)`
109
+ # in Rack middleware.
110
+ def header_hash_for(request)
111
+ config = config_for(request)
112
+
113
+ headers = if cached_headers = config.cached_headers
114
+ use_cached_headers(cached_headers, request)
115
+ else
116
+ build_headers(config, request)
80
117
  end
81
118
 
82
- config = self.class.secure_headers_options[:csp] if config.nil?
83
- config = self.class.options_for :csp, config
84
-
85
- return if config == false
119
+ headers
120
+ end
86
121
 
87
- if config && config[:script_hash_middleware]
88
- ContentSecurityPolicy.add_to_env(request, self, config)
122
+ # Public: specify which named override will be used for this request.
123
+ # Raises an argument error if no named override exists.
124
+ #
125
+ # name - the name of the previously configured override.
126
+ def use_secure_headers_override(request, name)
127
+ if config = Configuration.get(name)
128
+ override_secure_headers_request_config(request, config)
89
129
  else
90
- csp_header = ContentSecurityPolicy.new(config, :request => request, :controller => self)
91
- set_header(csp_header)
130
+ raise ArgumentError.new("no override by the name of #{name} has been configured")
92
131
  end
93
132
  end
94
133
 
134
+ # Public: gets or creates a nonce for CSP.
135
+ #
136
+ # The nonce will be added to script_src
137
+ #
138
+ # Returns the nonce
139
+ def content_security_policy_script_nonce(request)
140
+ content_security_policy_nonce(request, CSP::SCRIPT_SRC)
141
+ end
95
142
 
96
- def prep_script_hash
97
- if ::SecureHeaders::Configuration.script_hashes
98
- @script_hashes = ::SecureHeaders::Configuration.script_hashes.dup
99
- ActiveSupport::Notifications.subscribe("render_partial.action_view") do |event_name, start_at, end_at, id, payload|
100
- save_hash_for_later payload
101
- end
102
-
103
- ActiveSupport::Notifications.subscribe("render_template.action_view") do |event_name, start_at, end_at, id, payload|
104
- save_hash_for_later payload
105
- end
106
- end
143
+ # Public: gets or creates a nonce for CSP.
144
+ #
145
+ # The nonce will be added to style_src
146
+ #
147
+ # Returns the nonce
148
+ def content_security_policy_style_nonce(request)
149
+ content_security_policy_nonce(request, CSP::STYLE_SRC)
107
150
  end
108
151
 
109
- def save_hash_for_later payload
110
- matching_hashes = @script_hashes[payload[:identifier].gsub(Rails.root.to_s + "/", "")] || []
152
+ private
111
153
 
112
- if payload[:layout]
113
- # We're assuming an html.erb layout for now. Will need to handle mustache too, just not sure of the best way to do this
114
- layout_hashes = @script_hashes[File.join("app", "views", payload[:layout]) + '.html.erb']
154
+ # Private: gets or creates a nonce for CSP.
155
+ #
156
+ # Returns the nonce
157
+ def content_security_policy_nonce(request, script_or_style)
158
+ request.env[NONCE_KEY] ||= SecureRandom.base64(32).chomp
159
+ nonce_key = script_or_style == CSP::SCRIPT_SRC ? :script_nonce : :style_nonce
160
+ append_content_security_policy_directives(request, nonce_key => request.env[NONCE_KEY])
161
+ request.env[NONCE_KEY]
162
+ end
115
163
 
116
- matching_hashes << layout_hashes if layout_hashes
117
- end
164
+ # Private: convenience method for specifying which configuration object should
165
+ # be used for this request.
166
+ #
167
+ # Returns the config.
168
+ def override_secure_headers_request_config(request, config)
169
+ request.env[SECURE_HEADERS_CONFIG] = config
170
+ end
118
171
 
119
- if matching_hashes.any?
120
- request.env[HASHES_ENV_KEY] = ((request.env[HASHES_ENV_KEY] || []) << matching_hashes).flatten
172
+ # Private: determines which headers are applicable to a given request.
173
+ #
174
+ # Returns a list of classes whose corresponding header values are valid for
175
+ # this request.
176
+ def header_classes_for(request)
177
+ if request.scheme == HTTPS
178
+ ALL_HEADER_CLASSES
179
+ else
180
+ HTTP_HEADER_CLASSES
121
181
  end
122
182
  end
123
183
 
124
- def set_x_frame_options_header(options=self.class.secure_headers_options[:x_frame_options])
125
- set_a_header(:x_frame_options, XFrameOptions, options)
126
- end
184
+ # Private: do the heavy lifting of converting a configuration object
185
+ # to a hash of headers valid for this request.
186
+ #
187
+ # Returns a hash of header names / values.
188
+ def build_headers(config, request)
189
+ header_classes_for(request).each_with_object({}) do |klass, hash|
190
+ header_config = if config
191
+ config.fetch(klass::CONFIG_KEY)
192
+ end
127
193
 
128
- def set_x_content_type_options_header(options=self.class.secure_headers_options[:x_content_type_options])
129
- set_a_header(:x_content_type_options, XContentTypeOptions, options)
194
+ header_name, value = if klass == CSP
195
+ make_header(klass, header_config, request.user_agent)
196
+ else
197
+ make_header(klass, header_config)
198
+ end
199
+ hash[header_name] = value if value
200
+ end
130
201
  end
131
202
 
132
- def set_x_xss_protection_header(options=self.class.secure_headers_options[:x_xss_protection])
133
- set_a_header(:x_xss_protection, XXssProtection, options)
203
+ # Private: takes a precomputed hash of headers and returns the Headers
204
+ # customized for the request.
205
+ #
206
+ # Returns a hash of header names / values valid for a given request.
207
+ def use_cached_headers(default_headers, request)
208
+ header_classes_for(request).each_with_object({}) do |klass, hash|
209
+ if default_header = default_headers[klass::CONFIG_KEY]
210
+ header_name, value = if klass == CSP
211
+ default_csp_header_for_ua(default_header, request)
212
+ else
213
+ default_header
214
+ end
215
+ hash[header_name] = value
216
+ end
217
+ end
134
218
  end
135
219
 
136
- def set_hsts_header(options=self.class.secure_headers_options[:hsts])
137
- return unless request.ssl?
138
- set_a_header(:hsts, StrictTransportSecurity, options)
220
+ # Private: Retreives the config for a given header type:
221
+ #
222
+ # Checks to see if there is an override for this request, then
223
+ # Checks to see if a named override is used for this request, then
224
+ # Falls back to the global config
225
+ def config_for(request)
226
+ request.env[SECURE_HEADERS_CONFIG] ||
227
+ Configuration.get(Configuration::DEFAULT_CONFIG)
139
228
  end
140
229
 
141
- def set_hpkp_header(options=self.class.secure_headers_options[:hpkp])
142
- return unless request.ssl?
143
- config = self.class.options_for :hpkp, options
144
-
145
- return if config == false || config.nil?
146
-
147
- hpkp_header = PublicKeyPins.new(config)
148
- set_header(hpkp_header)
230
+ # Private: chooses the applicable CSP header for the provided user agent.
231
+ #
232
+ # headers - a hash of header_config_key => [header_name, header_value]
233
+ #
234
+ # Returns a CSP [header, value] array
235
+ def default_csp_header_for_ua(headers, request)
236
+ family = UserAgent.parse(request.user_agent).browser
237
+ if CSP::VARIATIONS.key?(family)
238
+ headers[family]
239
+ else
240
+ headers[CSP::OTHER]
241
+ end
149
242
  end
150
243
 
151
- def set_x_download_options_header(options=self.class.secure_headers_options[:x_download_options])
152
- set_a_header(:x_download_options, XDownloadOptions, options)
244
+ # Private: optionally build a header with a given configure
245
+ #
246
+ # klass - corresponding Class for a given header
247
+ # config - A string, symbol, or hash config for the header
248
+ # user_agent - A string representing the UA (only used for CSP feature sniffing)
249
+ #
250
+ # Returns a 2 element array [header_name, header_value] or nil if config
251
+ # is OPT_OUT
252
+ def make_header(klass, header_config, user_agent = nil)
253
+ unless header_config == OPT_OUT
254
+ if klass == CSP
255
+ klass.make_header(header_config, user_agent)
256
+ else
257
+ klass.make_header(header_config)
258
+ end
259
+ end
153
260
  end
261
+ end
154
262
 
155
- def set_x_permitted_cross_domain_policies_header(options=self.class.secure_headers_options[:x_permitted_cross_domain_policies])
156
- set_a_header(:x_permitted_cross_domain_policies, XPermittedCrossDomainPolicies, options)
157
- end
263
+ # These methods are mixed into controllers and delegate to the class method
264
+ # with the same name.
265
+ def use_secure_headers_override(name)
266
+ SecureHeaders.use_secure_headers_override(request, name)
267
+ end
158
268
 
159
- private
269
+ def content_security_policy_script_nonce
270
+ SecureHeaders.content_security_policy_script_nonce(request)
271
+ end
160
272
 
161
- def set_a_header(name, klass, options=nil)
162
- options = self.class.options_for name, options
163
- return if options == false
273
+ def content_security_policy_style_nonce
274
+ SecureHeaders.content_security_policy_style_nonce(request)
275
+ end
164
276
 
165
- header = klass.new(options)
166
- set_header(header)
167
- end
277
+ def opt_out_of_header(header_key)
278
+ SecureHeaders.opt_out_of_header(request, header_key)
279
+ end
168
280
 
169
- def set_header(name_or_header, value=nil)
170
- if name_or_header.is_a?(Header)
171
- header = name_or_header
172
- response.headers[header.name] = header.value
173
- else
174
- response.headers[name_or_header] = value
175
- end
176
- end
281
+ def append_content_security_policy_directives(additions)
282
+ SecureHeaders.append_content_security_policy_directives(request, additions)
177
283
  end
178
- end
179
284
 
285
+ def override_content_security_policy_directives(additions)
286
+ SecureHeaders.override_content_security_policy_directives(request, additions)
287
+ end
180
288
 
181
- require "secure_headers/version"
182
- require "secure_headers/header"
183
- require "secure_headers/headers/public_key_pins"
184
- require "secure_headers/headers/content_security_policy"
185
- require "secure_headers/headers/x_frame_options"
186
- require "secure_headers/headers/strict_transport_security"
187
- require "secure_headers/headers/x_xss_protection"
188
- require "secure_headers/headers/x_content_type_options"
189
- require "secure_headers/headers/x_download_options"
190
- require "secure_headers/headers/x_permitted_cross_domain_policies"
191
- require "secure_headers/railtie"
192
- require "secure_headers/hash_helper"
193
- require "secure_headers/view_helper"
289
+ def override_x_frame_options(value)
290
+ SecureHeaders.override_x_frame_options(request, value)
291
+ end
292
+ end
@@ -1,23 +1,19 @@
1
1
  # -*- encoding: utf-8 -*-
2
- lib = File.expand_path('../lib', __FILE__)
3
- $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
- require 'secure_headers/version'
5
-
6
2
  Gem::Specification.new do |gem|
7
3
  gem.name = "secure_headers"
8
- gem.version = SecureHeaders::VERSION
4
+ gem.version = "3.0.0"
9
5
  gem.authors = ["Neil Matatall"]
10
6
  gem.email = ["neil.matatall@gmail.com"]
11
- gem.description = %q{Security related headers all in one gem.}
12
- gem.summary = %q{Add easily configured security headers to responses
7
+ gem.description = 'Security related headers all in one gem.'
8
+ gem.summary = 'Add easily configured security headers to responses
13
9
  including content-security-policy, x-frame-options,
14
- strict-transport-security, etc.}
10
+ strict-transport-security, etc.'
15
11
  gem.homepage = "https://github.com/twitter/secureheaders"
16
12
  gem.license = "Apache Public License 2.0"
17
- gem.files = `git ls-files`.split($/)
18
- gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
13
+ gem.files = `git ls-files`.split($INPUT_RECORD_SEPARATOR)
14
+ gem.executables = gem.files.grep(%r{^bin/}).map { |f| File.basename(f) }
19
15
  gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
20
16
  gem.require_paths = ["lib"]
21
17
  gem.add_development_dependency "rake"
22
- gem.post_install_message = "Warning: lambda config values will be broken until you add |controller|. e.g. :enforce => lambda { |controller| some_expression }"
18
+ gem.add_dependency "useragent"
23
19
  end
@@ -0,0 +1,80 @@
1
+ require 'spec_helper'
2
+
3
+ module SecureHeaders
4
+ describe Configuration do
5
+ before(:each) do
6
+ reset_config
7
+ Configuration.default
8
+ end
9
+
10
+ it "has a default config" do
11
+ expect(Configuration.get(Configuration::DEFAULT_CONFIG)).to_not be_nil
12
+ end
13
+
14
+ it "has an 'noop' config" do
15
+ expect(Configuration.get(Configuration::NOOP_CONFIGURATION)).to_not be_nil
16
+ end
17
+
18
+ it "precomputes headers upon creation" do
19
+ default_config = Configuration.get(Configuration::DEFAULT_CONFIG)
20
+ header_hash = default_config.cached_headers.each_with_object({}) do |(key, value), hash|
21
+ header_name, header_value = if key == :csp
22
+ value["Chrome"]
23
+ else
24
+ value
25
+ end
26
+
27
+ hash[header_name] = header_value
28
+ end
29
+ expect_default_values(header_hash)
30
+ end
31
+
32
+ it "copies all config values except for the cached headers when dup" do
33
+ Configuration.override(:test_override, Configuration::NOOP_CONFIGURATION) do
34
+ # do nothing, just copy it
35
+ end
36
+
37
+ config = Configuration.get(:test_override)
38
+ noop = Configuration.get(Configuration::NOOP_CONFIGURATION)
39
+ [:hsts, :x_frame_options, :x_content_type_options, :x_xss_protection,
40
+ :x_download_options, :x_permitted_cross_domain_policies, :hpkp, :csp].each do |key|
41
+
42
+ expect(config.send(key)).to eq(noop.send(key)), "Value not copied: #{key}."
43
+ end
44
+ end
45
+
46
+ it "stores an override of the global config" do
47
+ Configuration.override(:test_override) do |config|
48
+ config.x_frame_options = "DENY"
49
+ end
50
+
51
+ expect(Configuration.get(:test_override)).to_not be_nil
52
+ end
53
+
54
+ it "deep dup's config values when overriding so the original cannot be modified" do
55
+ Configuration.override(:override) do |config|
56
+ config.csp[:default_src] << "'self'"
57
+ end
58
+
59
+ default = Configuration.get
60
+ override = Configuration.get(:override)
61
+
62
+ expect(override.csp).not_to eq(default.csp)
63
+ end
64
+
65
+ it "allows you to override an override" do
66
+ Configuration.override(:override) do |config|
67
+ config.csp = { default_src: %w('self')}
68
+ end
69
+
70
+ Configuration.override(:second_override, :override) do |config|
71
+ config.csp = config.csp.merge(script_src: %w(example.org))
72
+ end
73
+
74
+ original_override = Configuration.get(:override)
75
+ expect(original_override.csp).to eq(default_src: %w('self'))
76
+ override_config = Configuration.get(:second_override)
77
+ expect(override_config.csp).to eq(default_src: %w('self'), script_src: %w(example.org))
78
+ end
79
+ end
80
+ end