secure_headers 2.5.3 → 3.0.0.pre

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 (125) hide show
  1. checksums.yaml +4 -4
  2. data/.rspec +1 -0
  3. data/.travis.yml +2 -1
  4. data/Gemfile +9 -16
  5. data/README.md +154 -331
  6. data/Rakefile +2 -36
  7. data/lib/secure_headers/configuration.rb +189 -0
  8. data/lib/secure_headers/headers/content_security_policy.rb +341 -254
  9. data/lib/secure_headers/headers/public_key_pins.rb +43 -58
  10. data/lib/secure_headers/headers/strict_transport_security.rb +21 -49
  11. data/lib/secure_headers/headers/x_content_type_options.rb +18 -33
  12. data/lib/secure_headers/headers/x_download_options.rb +18 -33
  13. data/lib/secure_headers/headers/x_frame_options.rb +24 -34
  14. data/lib/secure_headers/headers/x_permitted_cross_domain_policies.rb +19 -34
  15. data/lib/secure_headers/headers/x_xss_protection.rb +17 -48
  16. data/lib/secure_headers/middleware.rb +15 -0
  17. data/lib/secure_headers/padrino.rb +1 -2
  18. data/lib/secure_headers/railtie.rb +9 -6
  19. data/lib/secure_headers/view_helper.rb +27 -43
  20. data/lib/secure_headers.rb +254 -61
  21. data/secure_headers.gemspec +7 -12
  22. data/spec/lib/secure_headers/configuration_spec.rb +80 -0
  23. data/spec/lib/secure_headers/headers/content_security_policy_spec.rb +111 -276
  24. data/spec/lib/secure_headers/headers/public_key_pins_spec.rb +17 -17
  25. data/spec/lib/secure_headers/headers/strict_transport_security_spec.rb +11 -43
  26. data/spec/lib/secure_headers/headers/x_content_type_options_spec.rb +11 -18
  27. data/spec/lib/secure_headers/headers/x_download_options_spec.rb +13 -17
  28. data/spec/lib/secure_headers/headers/x_frame_options_spec.rb +15 -17
  29. data/spec/lib/secure_headers/headers/x_permitted_cross_domain_policies_spec.rb +22 -39
  30. data/spec/lib/secure_headers/headers/x_xss_protection_spec.rb +20 -30
  31. data/spec/lib/secure_headers/middleware_spec.rb +40 -0
  32. data/spec/lib/secure_headers_spec.rb +201 -339
  33. data/spec/spec_helper.rb +30 -30
  34. data/upgrading-to-3-0.md +35 -0
  35. metadata +14 -100
  36. data/fixtures/rails_3_2_22/.rspec +0 -1
  37. data/fixtures/rails_3_2_22/Gemfile +0 -6
  38. data/fixtures/rails_3_2_22/README.rdoc +0 -261
  39. data/fixtures/rails_3_2_22/Rakefile +0 -7
  40. data/fixtures/rails_3_2_22/app/controllers/application_controller.rb +0 -4
  41. data/fixtures/rails_3_2_22/app/controllers/other_things_controller.rb +0 -5
  42. data/fixtures/rails_3_2_22/app/controllers/things_controller.rb +0 -5
  43. data/fixtures/rails_3_2_22/app/models/.gitkeep +0 -0
  44. data/fixtures/rails_3_2_22/app/views/layouts/application.html.erb +0 -11
  45. data/fixtures/rails_3_2_22/app/views/other_things/index.html.erb +0 -2
  46. data/fixtures/rails_3_2_22/app/views/things/index.html.erb +0 -1
  47. data/fixtures/rails_3_2_22/config/application.rb +0 -14
  48. data/fixtures/rails_3_2_22/config/boot.rb +0 -6
  49. data/fixtures/rails_3_2_22/config/environment.rb +0 -5
  50. data/fixtures/rails_3_2_22/config/environments/test.rb +0 -37
  51. data/fixtures/rails_3_2_22/config/initializers/secure_headers.rb +0 -16
  52. data/fixtures/rails_3_2_22/config/routes.rb +0 -4
  53. data/fixtures/rails_3_2_22/config/script_hashes.yml +0 -5
  54. data/fixtures/rails_3_2_22/config.ru +0 -7
  55. data/fixtures/rails_3_2_22/lib/assets/.gitkeep +0 -0
  56. data/fixtures/rails_3_2_22/lib/tasks/.gitkeep +0 -0
  57. data/fixtures/rails_3_2_22/log/.gitkeep +0 -0
  58. data/fixtures/rails_3_2_22/spec/controllers/other_things_controller_spec.rb +0 -83
  59. data/fixtures/rails_3_2_22/spec/controllers/things_controller_spec.rb +0 -54
  60. data/fixtures/rails_3_2_22/spec/spec_helper.rb +0 -15
  61. data/fixtures/rails_3_2_22/vendor/assets/javascripts/.gitkeep +0 -0
  62. data/fixtures/rails_3_2_22/vendor/assets/stylesheets/.gitkeep +0 -0
  63. data/fixtures/rails_3_2_22/vendor/plugins/.gitkeep +0 -0
  64. data/fixtures/rails_3_2_22_no_init/.rspec +0 -1
  65. data/fixtures/rails_3_2_22_no_init/Gemfile +0 -6
  66. data/fixtures/rails_3_2_22_no_init/README.rdoc +0 -261
  67. data/fixtures/rails_3_2_22_no_init/Rakefile +0 -7
  68. data/fixtures/rails_3_2_22_no_init/app/controllers/application_controller.rb +0 -4
  69. data/fixtures/rails_3_2_22_no_init/app/controllers/other_things_controller.rb +0 -20
  70. data/fixtures/rails_3_2_22_no_init/app/controllers/things_controller.rb +0 -5
  71. data/fixtures/rails_3_2_22_no_init/app/models/.gitkeep +0 -0
  72. data/fixtures/rails_3_2_22_no_init/app/views/layouts/application.html.erb +0 -12
  73. data/fixtures/rails_3_2_22_no_init/app/views/other_things/index.html.erb +0 -1
  74. data/fixtures/rails_3_2_22_no_init/app/views/things/index.html.erb +0 -0
  75. data/fixtures/rails_3_2_22_no_init/config/application.rb +0 -17
  76. data/fixtures/rails_3_2_22_no_init/config/boot.rb +0 -6
  77. data/fixtures/rails_3_2_22_no_init/config/environment.rb +0 -5
  78. data/fixtures/rails_3_2_22_no_init/config/environments/test.rb +0 -37
  79. data/fixtures/rails_3_2_22_no_init/config/routes.rb +0 -4
  80. data/fixtures/rails_3_2_22_no_init/config.ru +0 -4
  81. data/fixtures/rails_3_2_22_no_init/lib/assets/.gitkeep +0 -0
  82. data/fixtures/rails_3_2_22_no_init/lib/tasks/.gitkeep +0 -0
  83. data/fixtures/rails_3_2_22_no_init/log/.gitkeep +0 -0
  84. data/fixtures/rails_3_2_22_no_init/spec/controllers/other_things_controller_spec.rb +0 -56
  85. data/fixtures/rails_3_2_22_no_init/spec/controllers/things_controller_spec.rb +0 -54
  86. data/fixtures/rails_3_2_22_no_init/spec/spec_helper.rb +0 -5
  87. data/fixtures/rails_3_2_22_no_init/vendor/assets/javascripts/.gitkeep +0 -0
  88. data/fixtures/rails_3_2_22_no_init/vendor/assets/stylesheets/.gitkeep +0 -0
  89. data/fixtures/rails_3_2_22_no_init/vendor/plugins/.gitkeep +0 -0
  90. data/fixtures/rails_4_1_8/Gemfile +0 -5
  91. data/fixtures/rails_4_1_8/README.rdoc +0 -28
  92. data/fixtures/rails_4_1_8/Rakefile +0 -6
  93. data/fixtures/rails_4_1_8/app/controllers/application_controller.rb +0 -4
  94. data/fixtures/rails_4_1_8/app/controllers/concerns/.keep +0 -0
  95. data/fixtures/rails_4_1_8/app/controllers/other_things_controller.rb +0 -5
  96. data/fixtures/rails_4_1_8/app/controllers/things_controller.rb +0 -5
  97. data/fixtures/rails_4_1_8/app/models/.keep +0 -0
  98. data/fixtures/rails_4_1_8/app/models/concerns/.keep +0 -0
  99. data/fixtures/rails_4_1_8/app/views/layouts/application.html.erb +0 -11
  100. data/fixtures/rails_4_1_8/app/views/other_things/index.html.erb +0 -2
  101. data/fixtures/rails_4_1_8/app/views/things/index.html.erb +0 -1
  102. data/fixtures/rails_4_1_8/config/application.rb +0 -15
  103. data/fixtures/rails_4_1_8/config/boot.rb +0 -4
  104. data/fixtures/rails_4_1_8/config/environment.rb +0 -5
  105. data/fixtures/rails_4_1_8/config/environments/test.rb +0 -10
  106. data/fixtures/rails_4_1_8/config/initializers/secure_headers.rb +0 -16
  107. data/fixtures/rails_4_1_8/config/routes.rb +0 -4
  108. data/fixtures/rails_4_1_8/config/script_hashes.yml +0 -5
  109. data/fixtures/rails_4_1_8/config/secrets.yml +0 -22
  110. data/fixtures/rails_4_1_8/config.ru +0 -4
  111. data/fixtures/rails_4_1_8/lib/assets/.keep +0 -0
  112. data/fixtures/rails_4_1_8/lib/tasks/.keep +0 -0
  113. data/fixtures/rails_4_1_8/log/.keep +0 -0
  114. data/fixtures/rails_4_1_8/spec/controllers/other_things_controller_spec.rb +0 -83
  115. data/fixtures/rails_4_1_8/spec/controllers/things_controller_spec.rb +0 -59
  116. data/fixtures/rails_4_1_8/spec/spec_helper.rb +0 -15
  117. data/fixtures/rails_4_1_8/vendor/assets/javascripts/.keep +0 -0
  118. data/fixtures/rails_4_1_8/vendor/assets/stylesheets/.keep +0 -0
  119. data/lib/secure_headers/controller_extension.rb +0 -158
  120. data/lib/secure_headers/hash_helper.rb +0 -7
  121. data/lib/secure_headers/header.rb +0 -5
  122. data/lib/secure_headers/headers/content_security_policy/script_hash_middleware.rb +0 -22
  123. data/lib/secure_headers/version.rb +0 -3
  124. data/lib/tasks/tasks.rake +0 -48
  125. data/spec/lib/secure_headers/headers/content_security_policy/script_hash_middleware_spec.rb +0 -46
@@ -1,61 +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 = nil, &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 = nil, &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
- request.env[HASHES_ENV_KEY] = (request.env[HASHES_ENV_KEY] || []) << hash_value
31
- end
32
- 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)
33
29
  end
34
-
35
- content_tag :script, content
36
30
  end
37
31
 
38
32
  private
39
33
 
40
- def nonced_tag(content, type, block)
34
+ def nonced_tag(type, content_or_options, block)
35
+ options = {}
41
36
  content = if block
37
+ options = content_or_options
42
38
  capture(&block)
43
39
  else
44
- content.html_safe # :'(
40
+ content_or_options.html_safe # :'(
45
41
  end
46
-
47
- content_tag type, content, :nonce => @content_security_policy_nonce
48
- end
49
-
50
- def unexpected_hash_error_message(file_path, hash_value, content)
51
- <<-EOF
52
- \n\n*** WARNING: Unrecognized hash in #{file_path}!!! Value: #{hash_value} ***
53
- <script>#{content}</script>
54
- *** This is fine in dev/test, but will raise exceptions in production. ***
55
- *** Run #{SECURE_HEADERS_RAKE_TASK} or add the following to config/script_hashes.yml:***
56
- #{file_path}:
57
- - #{hash_value}\n\n
58
- EOF
42
+ content_tag type, content, options.merge(nonce: content_security_policy_nonce(type))
59
43
  end
60
44
  end
61
45
  end
@@ -1,5 +1,4 @@
1
- require "secure_headers/version"
2
- require "secure_headers/header"
1
+ require "secure_headers/configuration"
3
2
  require "secure_headers/headers/public_key_pins"
4
3
  require "secure_headers/headers/content_security_policy"
5
4
  require "secure_headers/headers/x_frame_options"
@@ -8,86 +7,280 @@ require "secure_headers/headers/x_xss_protection"
8
7
  require "secure_headers/headers/x_content_type_options"
9
8
  require "secure_headers/headers/x_download_options"
10
9
  require "secure_headers/headers/x_permitted_cross_domain_policies"
11
- require "secure_headers/controller_extension"
10
+ require "secure_headers/middleware"
12
11
  require "secure_headers/railtie"
13
- require "secure_headers/hash_helper"
14
12
  require "secure_headers/view_helper"
13
+ require "useragent"
15
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
16
17
  module SecureHeaders
17
- SCRIPT_HASH_CONFIG_FILE = 'config/script_hashes.yml'
18
- HASHES_ENV_KEY = 'secure_headers.script_hashes'
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
19
23
 
20
24
  ALL_HEADER_CLASSES = [
21
- SecureHeaders::ContentSecurityPolicy,
22
- SecureHeaders::StrictTransportSecurity,
23
- SecureHeaders::PublicKeyPins,
24
- SecureHeaders::XContentTypeOptions,
25
- SecureHeaders::XDownloadOptions,
26
- SecureHeaders::XFrameOptions,
27
- SecureHeaders::XPermittedCrossDomainPolicies,
28
- SecureHeaders::XXssProtection
29
- ]
30
-
31
- ALL_FILTER_METHODS = [
32
- :prep_script_hash,
33
- :set_hsts_header,
34
- :set_hpkp_header,
35
- :set_x_frame_options_header,
36
- :set_csp_header,
37
- :set_x_xss_protection_header,
38
- :set_x_content_type_options_header,
39
- :set_x_download_options_header,
40
- :set_x_permitted_cross_domain_policies_header
41
- ]
42
-
43
- module Configuration
44
- class << self
45
- attr_accessor :hsts, :x_frame_options, :x_content_type_options,
46
- :x_xss_protection, :csp, :x_download_options, :script_hashes,
47
- :x_permitted_cross_domain_policies, :hpkp
48
-
49
- # For preparation for the secure_headers 3.x change.
50
- def default &block
51
- instance_eval &block
52
- if File.exists?(SCRIPT_HASH_CONFIG_FILE)
53
- ::SecureHeaders::Configuration.script_hashes = YAML.load(File.open(SCRIPT_HASH_CONFIG_FILE))
54
- end
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).dup
51
+ if config.csp == OPT_OUT
52
+ config.csp = {}
53
+ end
54
+ config.csp.merge!(additions)
55
+ override_secure_headers_request_config(request, config)
56
+ end
57
+
58
+ # Public: appends source values to the current configuration. If no value
59
+ # is set for a given directive, the value will be merged with the default-src
60
+ # value. If a value exists for the given directive, the values will be combined.
61
+ #
62
+ # additions - a hash containing directives. e.g.
63
+ # script_src: %w(another-host.com)
64
+ def append_content_security_policy_directives(request, additions)
65
+ config = config_for(request).dup
66
+ config.csp = CSP.combine_policies(config.csp, additions)
67
+ override_secure_headers_request_config(request, config)
68
+ end
69
+
70
+ # Public: override X-Frame-Options settings for this request.
71
+ #
72
+ # value - deny, sameorigin, or allowall
73
+ #
74
+ # Returns the current config
75
+ def override_x_frame_options(request, value)
76
+ default_config = config_for(request).dup
77
+ default_config.x_frame_options = value
78
+ override_secure_headers_request_config(request, default_config)
79
+ end
80
+
81
+ # Public: opts out of setting a given header by creating a temporary config
82
+ # and setting the given headers config to OPT_OUT.
83
+ def opt_out_of_header(request, header_key)
84
+ config = config_for(request).dup
85
+ config.send("#{header_key}=", OPT_OUT)
86
+ override_secure_headers_request_config(request, config)
87
+ end
88
+
89
+ # Public: opts out of setting all headers by telling secure_headers to use
90
+ # the NOOP configuration.
91
+ def opt_out_of_all_protection(request)
92
+ use_secure_headers_override(request, Configuration::NOOP_CONFIGURATION)
93
+ end
94
+
95
+ # Public: Builds the hash of headers that should be applied base on the
96
+ # request.
97
+ #
98
+ # StrictTransportSecurity and PublicKeyPins are not applied to http requests.
99
+ # See #config_for to determine which config is used for a given request.
100
+ #
101
+ # Returns a hash of header names => header values. The value
102
+ # returned is meant to be merged into the header value from `@app.call(env)`
103
+ # in Rack middleware.
104
+ def header_hash_for(request)
105
+ config = config_for(request)
106
+
107
+ headers = if cached_headers = config.cached_headers
108
+ use_cached_headers(cached_headers, request)
109
+ else
110
+ build_headers(config, request)
55
111
  end
56
112
 
57
- def configure &block
58
- warn "[DEPRECATION] `configure` is removed in secure_headers 3.x. Instead use `default`."
59
- default &block
113
+ headers
114
+ end
115
+
116
+ # Public: specify which named override will be used for this request.
117
+ # Raises an argument error if no named override exists.
118
+ #
119
+ # name - the name of the previously configured override.
120
+ def use_secure_headers_override(request, name)
121
+ if config = Configuration.get(name)
122
+ override_secure_headers_request_config(request, config)
123
+ else
124
+ raise ArgumentError.new("no override by the name of #{name} has been configured")
60
125
  end
61
126
  end
62
- end
63
127
 
64
- class << self
65
- def append_features(base)
66
- base.send(:include, ControllerExtension)
128
+ # Public: gets or creates a nonce for CSP.
129
+ #
130
+ # The nonce will be added to script_src
131
+ #
132
+ # Returns the nonce
133
+ def content_security_policy_script_nonce(request)
134
+ content_security_policy_nonce(request, CSP::SCRIPT_SRC)
135
+ end
136
+
137
+ # Public: gets or creates a nonce for CSP.
138
+ #
139
+ # The nonce will be added to style_src
140
+ #
141
+ # Returns the nonce
142
+ def content_security_policy_style_nonce(request)
143
+ content_security_policy_nonce(request, CSP::STYLE_SRC)
144
+ end
145
+
146
+ private
147
+
148
+ # Private: gets or creates a nonce for CSP.
149
+ #
150
+ # Returns the nonce
151
+ def content_security_policy_nonce(request, script_or_style)
152
+ request.env[NONCE_KEY] ||= SecureRandom.base64(32).chomp
153
+ nonce_key = script_or_style == CSP::SCRIPT_SRC ? :script_nonce : :style_nonce
154
+ append_content_security_policy_directives(request, nonce_key => request.env[NONCE_KEY])
155
+ request.env[NONCE_KEY]
156
+ end
157
+
158
+ # Private: convenience method for specifying which configuration object should
159
+ # be used for this request.
160
+ #
161
+ # Returns the config.
162
+ def override_secure_headers_request_config(request, config)
163
+ request.env[SECURE_HEADERS_CONFIG] = config
67
164
  end
68
165
 
69
- def header_hash(options = nil)
70
- ALL_HEADER_CLASSES.inject({}) do |memo, klass|
71
- # must use !options[key].nil? because 'false' represents opting out, nil
72
- # represents use global default.
73
- config = if options.is_a?(Hash) && !options[klass::Constants::CONFIG_KEY].nil?
74
- options[klass::Constants::CONFIG_KEY]
166
+ # Private: determines which headers are applicable to a given request.
167
+ #
168
+ # Returns a list of classes whose corresponding header values are valid for
169
+ # this request.
170
+ def header_classes_for(request)
171
+ if request.scheme == HTTPS
172
+ ALL_HEADER_CLASSES
173
+ else
174
+ HTTP_HEADER_CLASSES
175
+ end
176
+ end
177
+
178
+ # Private: do the heavy lifting of converting a configuration object
179
+ # to a hash of headers valid for this request.
180
+ #
181
+ # Returns a hash of header names / values.
182
+ def build_headers(config, request)
183
+ header_classes_for(request).each_with_object({}) do |klass, hash|
184
+ header_config = if config
185
+ config.fetch(klass::CONFIG_KEY)
186
+ end
187
+
188
+ header_name, value = if klass == CSP
189
+ make_header(klass, header_config, request.user_agent)
75
190
  else
76
- ::SecureHeaders::Configuration.send(klass::Constants::CONFIG_KEY)
191
+ make_header(klass, header_config)
77
192
  end
193
+ hash[header_name] = value if value
194
+ end
195
+ end
78
196
 
79
- unless klass == SecureHeaders::PublicKeyPins && !config.is_a?(Hash)
80
- header = get_a_header(klass, config)
81
- memo[header.name] = header.value if header
197
+ # Private: takes a precomputed hash of headers and returns the Headers
198
+ # customized for the request.
199
+ #
200
+ # Returns a hash of header names / values valid for a given request.
201
+ def use_cached_headers(default_headers, request)
202
+ header_classes_for(request).each_with_object({}) do |klass, hash|
203
+ if default_header = default_headers[klass::CONFIG_KEY]
204
+ header_name, value = if klass == CSP
205
+ default_csp_header_for_ua(default_header, request)
206
+ else
207
+ default_header
208
+ end
209
+ hash[header_name] = value
82
210
  end
83
- memo
84
211
  end
85
212
  end
86
213
 
87
- def get_a_header(klass, options)
88
- return if options == false
89
- klass.new(options)
214
+ # Private: Retreives the config for a given header type:
215
+ #
216
+ # Checks to see if there is an override for this request, then
217
+ # Checks to see if a named override is used for this request, then
218
+ # Falls back to the global config
219
+ def config_for(request)
220
+ request.env[SECURE_HEADERS_CONFIG] ||
221
+ Configuration.get(Configuration::DEFAULT_CONFIG)
222
+ end
223
+
224
+ # Private: chooses the applicable CSP header for the provided user agent.
225
+ #
226
+ # headers - a hash of header_config_key => [header_name, header_value]
227
+ #
228
+ # Returns a CSP [header, value] array
229
+ def default_csp_header_for_ua(headers, request)
230
+ family = UserAgent.parse(request.user_agent).browser
231
+ if CSP::VARIATIONS.key?(family)
232
+ headers[family]
233
+ else
234
+ headers[CSP::OTHER]
235
+ end
236
+ end
237
+
238
+ # Private: optionally build a header with a given configure
239
+ #
240
+ # klass - corresponding Class for a given header
241
+ # config - A string, symbol, or hash config for the header
242
+ # user_agent - A string representing the UA (only used for CSP feature sniffing)
243
+ #
244
+ # Returns a 2 element array [header_name, header_value] or nil if config
245
+ # is OPT_OUT
246
+ def make_header(klass, header_config, user_agent = nil)
247
+ unless header_config == OPT_OUT
248
+ if klass == CSP
249
+ klass.make_header(header_config, user_agent)
250
+ else
251
+ klass.make_header(header_config)
252
+ end
253
+ end
90
254
  end
91
255
  end
92
256
 
257
+ # These methods are mixed into controllers and delegate to the class method
258
+ # with the same name.
259
+ def use_secure_headers_override(name)
260
+ SecureHeaders.use_secure_headers_override(request, name)
261
+ end
262
+
263
+ def content_security_policy_script_nonce
264
+ SecureHeaders.content_security_policy_script_nonce(request)
265
+ end
266
+
267
+ def content_security_policy_style_nonce
268
+ SecureHeaders.content_security_policy_style_nonce(request)
269
+ end
270
+
271
+ def opt_out_of_header(header_key)
272
+ SecureHeaders.opt_out_of_header(request, header_key)
273
+ end
274
+
275
+ def append_content_security_policy_directives(additions)
276
+ SecureHeaders.append_content_security_policy_directives(request, additions)
277
+ end
278
+
279
+ def override_content_security_policy_directives(additions)
280
+ SecureHeaders.override_content_security_policy_directives(request, additions)
281
+ end
282
+
283
+ def override_x_frame_options(value)
284
+ SecureHeaders.override_x_frame_options(request, value)
285
+ end
93
286
  end
@@ -1,24 +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.pre"
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.add_dependency "user_agent_parser"
23
- gem.post_install_message = "[DEPRECATION] Development has stopped on the 2.x line of secure_headers. It will be maintained, but new features will be added to the 3.x branch. A lot has changed in secure_headers 3.x. A migration guide can be found in the documentation: https://github.com/twitter/secureheaders/blob/master/upgrading-to-3-0.md."
18
+ gem.add_dependency "useragent"
24
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