secure_headers 2.3.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 -0
  5. data/CHANGELOG.md +205 -0
  6. data/Gemfile +9 -10
  7. data/Guardfile +12 -0
  8. data/README.md +169 -301
  9. data/Rakefile +2 -36
  10. data/lib/secure_headers/configuration.rb +189 -0
  11. data/lib/secure_headers/headers/content_security_policy.rb +362 -213
  12. data/lib/secure_headers/headers/public_key_pins.rb +43 -58
  13. data/lib/secure_headers/headers/strict_transport_security.rb +21 -48
  14. data/lib/secure_headers/headers/x_content_type_options.rb +18 -32
  15. data/lib/secure_headers/headers/x_download_options.rb +18 -32
  16. data/lib/secure_headers/headers/x_frame_options.rb +24 -33
  17. data/lib/secure_headers/headers/x_permitted_cross_domain_policies.rb +19 -33
  18. data/lib/secure_headers/headers/x_xss_protection.rb +17 -47
  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 +229 -162
  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 +175 -262
  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 +202 -326
  36. data/spec/spec_helper.rb +32 -28
  37. data/upgrading-to-3-0.md +37 -0
  38. metadata +14 -94
  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 -20
  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 -56
  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,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,218 +7,286 @@ 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"
10
+ require "secure_headers/middleware"
11
11
  require "secure_headers/railtie"
12
- require "secure_headers/hash_helper"
13
12
  require "secure_headers/view_helper"
13
+ require "useragent"
14
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
15
17
  module SecureHeaders
16
- SCRIPT_HASH_CONFIG_FILE = 'config/script_hashes.yml'
17
- 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
18
23
 
19
24
  ALL_HEADER_CLASSES = [
20
- SecureHeaders::ContentSecurityPolicy,
21
- SecureHeaders::StrictTransportSecurity,
22
- SecureHeaders::PublicKeyPins,
23
- SecureHeaders::XContentTypeOptions,
24
- SecureHeaders::XDownloadOptions,
25
- SecureHeaders::XFrameOptions,
26
- SecureHeaders::XPermittedCrossDomainPolicies,
27
- SecureHeaders::XXssProtection
28
- ]
29
-
30
- module Configuration
31
- class << self
32
- attr_accessor :hsts, :x_frame_options, :x_content_type_options,
33
- :x_xss_protection, :csp, :x_download_options, :script_hashes,
34
- :x_permitted_cross_domain_policies, :hpkp
35
-
36
- def configure &block
37
- instance_eval &block
38
- if File.exists?(SCRIPT_HASH_CONFIG_FILE)
39
- ::SecureHeaders::Configuration.script_hashes = YAML.load(File.open(SCRIPT_HASH_CONFIG_FILE))
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 = {}
40
55
  end
56
+ config.csp.merge!(additions)
57
+ override_secure_headers_request_config(request, config)
41
58
  end
42
59
  end
43
- end
44
60
 
45
- class << self
46
- def append_features(base)
47
- base.module_eval do
48
- extend ClassMethods
49
- 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)
50
73
  end
51
74
  end
52
75
 
53
- def header_hash(options = nil)
54
- ALL_HEADER_CLASSES.inject({}) do |memo, klass|
55
- config = if options.is_a?(Hash) && options[klass::Constants::CONFIG_KEY]
56
- options[klass::Constants::CONFIG_KEY]
57
- else
58
- ::SecureHeaders::Configuration.send(klass::Constants::CONFIG_KEY)
59
- 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)
85
+ end
60
86
 
61
- unless klass == SecureHeaders::PublicKeyPins && !config.is_a?(Hash)
62
- header = get_a_header(klass::Constants::CONFIG_KEY, klass, config)
63
- memo[header.name] = header.value
64
- end
65
- memo
66
- end
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)
67
93
  end
68
94
 
69
- def get_a_header(name, klass, options)
70
- return if options == false
71
- klass.new(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)
72
99
  end
73
- end
74
100
 
75
- module ClassMethods
76
- attr_writer :secure_headers_options
77
- def secure_headers_options
78
- if @secure_headers_options
79
- @secure_headers_options
80
- elsif superclass.respond_to?(:secure_headers_options) # stop at application_controller
81
- superclass.secure_headers_options
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)
82
115
  else
83
- {}
116
+ build_headers(config, request)
84
117
  end
118
+
119
+ headers
85
120
  end
86
121
 
87
- def ensure_security_headers options = {}
88
- if RUBY_VERSION == "1.8.7"
89
- warn "[DEPRECATION] secure_headers ruby 1.8.7 support will dropped in the next release"
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)
129
+ else
130
+ raise ArgumentError.new("no override by the name of #{name} has been configured")
90
131
  end
91
- self.secure_headers_options = options
92
- before_filter :prep_script_hash
93
- before_filter :set_hsts_header
94
- before_filter :set_hpkp_header
95
- before_filter :set_x_frame_options_header
96
- before_filter :set_csp_header
97
- before_filter :set_x_xss_protection_header
98
- before_filter :set_x_content_type_options_header
99
- before_filter :set_x_download_options_header
100
- before_filter :set_x_permitted_cross_domain_policies_header
101
132
  end
102
- end
103
133
 
104
- module InstanceMethods
105
- def set_security_headers(options = self.class.secure_headers_options)
106
- set_csp_header(request, options[:csp])
107
- set_hsts_header(options[:hsts])
108
- set_hpkp_header(options[:hpkp])
109
- set_x_frame_options_header(options[:x_frame_options])
110
- set_x_xss_protection_header(options[:x_xss_protection])
111
- set_x_content_type_options_header(options[:x_content_type_options])
112
- set_x_download_options_header(options[:x_download_options])
113
- set_x_permitted_cross_domain_policies_header(options[:x_permitted_cross_domain_policies])
114
- end
115
-
116
- # set_csp_header - uses the request accessor and SecureHeader::Configuration settings
117
- # set_csp_header(+Rack::Request+) - uses the parameter and and SecureHeader::Configuration settings
118
- # set_csp_header(+Hash+) - uses the request accessor and options from parameters
119
- # set_csp_header(+Rack::Request+, +Hash+)
120
- def set_csp_header(req = nil, config=nil)
121
- if req.is_a?(Hash) || req.is_a?(FalseClass)
122
- config = req
123
- end
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
142
+
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)
150
+ end
124
151
 
125
- config = self.class.secure_headers_options[:csp] if config.nil?
126
- config = secure_header_options_for :csp, config
152
+ private
127
153
 
128
- return if config == false
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
163
+
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
129
171
 
130
- if config && config[:script_hash_middleware]
131
- ContentSecurityPolicy.add_to_env(request, self, config)
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
132
179
  else
133
- csp_header = ContentSecurityPolicy.new(config, :request => request, :controller => self)
134
- set_header(csp_header)
180
+ HTTP_HEADER_CLASSES
135
181
  end
136
182
  end
137
183
 
138
-
139
- def prep_script_hash
140
- if ::SecureHeaders::Configuration.script_hashes
141
- @script_hashes = ::SecureHeaders::Configuration.script_hashes.dup
142
- ActiveSupport::Notifications.subscribe("render_partial.action_view") do |event_name, start_at, end_at, id, payload|
143
- save_hash_for_later payload
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)
144
192
  end
145
193
 
146
- ActiveSupport::Notifications.subscribe("render_template.action_view") do |event_name, start_at, end_at, id, payload|
147
- save_hash_for_later payload
194
+ header_name, value = if klass == CSP
195
+ make_header(klass, header_config, request.user_agent)
196
+ else
197
+ make_header(klass, header_config)
148
198
  end
199
+ hash[header_name] = value if value
149
200
  end
150
201
  end
151
202
 
152
- def save_hash_for_later payload
153
- matching_hashes = @script_hashes[payload[:identifier].gsub(Rails.root.to_s + "/", "")] || []
154
-
155
- if payload[:layout]
156
- # 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
157
- layout_hashes = @script_hashes[File.join("app", "views", payload[:layout]) + '.html.erb']
158
-
159
- matching_hashes << layout_hashes if layout_hashes
160
- end
161
-
162
- if matching_hashes.any?
163
- request.env[HASHES_ENV_KEY] = ((request.env[HASHES_ENV_KEY] || []) << matching_hashes).flatten
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
164
217
  end
165
218
  end
166
219
 
167
- def set_x_frame_options_header(options=self.class.secure_headers_options[:x_frame_options])
168
- set_a_header(:x_frame_options, XFrameOptions, options)
169
- end
170
-
171
- def set_x_content_type_options_header(options=self.class.secure_headers_options[:x_content_type_options])
172
- set_a_header(:x_content_type_options, XContentTypeOptions, 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)
173
228
  end
174
229
 
175
- def set_x_xss_protection_header(options=self.class.secure_headers_options[:x_xss_protection])
176
- set_a_header(:x_xss_protection, XXssProtection, options)
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
177
242
  end
178
243
 
179
- def set_hsts_header(options=self.class.secure_headers_options[:hsts])
180
- return unless request.ssl?
181
- set_a_header(:hsts, StrictTransportSecurity, 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
182
260
  end
261
+ end
183
262
 
184
- def set_hpkp_header(options=self.class.secure_headers_options[:hpkp])
185
- return unless request.ssl?
186
- config = secure_header_options_for :hpkp, options
187
-
188
- return if config == false || config.nil?
189
-
190
- hpkp_header = PublicKeyPins.new(config)
191
- set_header(hpkp_header)
192
- 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
193
268
 
194
- def set_x_download_options_header(options=self.class.secure_headers_options[:x_download_options])
195
- set_a_header(:x_download_options, XDownloadOptions, options)
196
- end
269
+ def content_security_policy_script_nonce
270
+ SecureHeaders.content_security_policy_script_nonce(request)
271
+ end
197
272
 
198
- def set_x_permitted_cross_domain_policies_header(options=self.class.secure_headers_options[:x_permitted_cross_domain_policies])
199
- set_a_header(:x_permitted_cross_domain_policies, XPermittedCrossDomainPolicies, options)
200
- end
273
+ def content_security_policy_style_nonce
274
+ SecureHeaders.content_security_policy_style_nonce(request)
275
+ end
201
276
 
202
- private
277
+ def opt_out_of_header(header_key)
278
+ SecureHeaders.opt_out_of_header(request, header_key)
279
+ end
203
280
 
204
- # we can't use ||= because I'm overloading false => disable, nil => default
205
- # both of which trigger the conditional assignment
206
- def secure_header_options_for(type, options)
207
- options.nil? ? ::SecureHeaders::Configuration.send(type) : options
208
- end
281
+ def append_content_security_policy_directives(additions)
282
+ SecureHeaders.append_content_security_policy_directives(request, additions)
283
+ end
209
284
 
210
- def set_a_header(name, klass, options=nil)
211
- options = secure_header_options_for(name, options)
212
- return if options == false
213
- set_header(SecureHeaders::get_a_header(name, klass, options))
214
- end
285
+ def override_content_security_policy_directives(additions)
286
+ SecureHeaders.override_content_security_policy_directives(request, additions)
287
+ end
215
288
 
216
- def set_header(name_or_header, value=nil)
217
- if name_or_header.is_a?(Header)
218
- header = name_or_header
219
- response.headers[header.name] = header.value
220
- else
221
- response.headers[name_or_header] = value
222
- end
223
- end
289
+ def override_x_frame_options(value)
290
+ SecureHeaders.override_x_frame_options(request, value)
224
291
  end
225
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.add_dependency "user_agent_parser"
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