Skip to content

Commit e45276b

Browse files
committed
Merge branch 'master' into script-hashes-for-3.x
2 parents 7126542 + 9756065 commit e45276b

File tree

10 files changed

+545
-28
lines changed

10 files changed

+545
-28
lines changed

README.md

Lines changed: 59 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ The gem will automatically apply several headers that are related to security.
1515
- X-Permitted-Cross-Domain-Policies - [Restrict Adobe Flash Player's access to data](https://www.adobe.com/devnet/adobe-media-server/articles/cross-domain-xml-for-streaming.html)
1616
- Public Key Pinning - Pin certificate fingerprints in the browser to prevent man-in-the-middle attacks due to compromised Certificate Authorities. [Public Key Pinning Specification](https://tools.ietf.org/html/rfc7469)
1717

18-
It can also mark all http cookies with the secure attribute (when configured to do so).
18+
It can also mark all http cookies with the Secure, HttpOnly and SameSite attributes (when configured to do so).
1919

2020
`secure_headers` is a library with a global config, per request overrides, and rack middleware that enables you customize your application settings.
2121

@@ -31,7 +31,13 @@ All `nil` values will fallback to their default values. `SecureHeaders::OPT_OUT`
3131

3232
```ruby
3333
SecureHeaders::Configuration.default do |config|
34-
config.secure_cookies = true # mark all cookies as "secure"
34+
config.cookies = {
35+
secure: true, # mark all cookies as "Secure"
36+
httponly: true, # mark all cookies as "HttpOnly"
37+
samesite: {
38+
strict: true # mark all cookies as SameSite=Strict
39+
}
40+
}
3541
config.hsts = "max-age=#{20.years.to_i}; includeSubdomains; preload"
3642
config.x_frame_options = "DENY"
3743
config.x_content_type_options = "nosniff"
@@ -322,6 +328,57 @@ config.hpkp = {
322328
}
323329
```
324330

331+
### Cookies
332+
333+
SecureHeaders supports `Secure`, `HttpOnly` and [`SameSite`](https://tools.ietf.org/html/draft-west-first-party-cookies-07) cookies. These can be defined in the form of a boolean, or as a Hash for more refined configuration.
334+
335+
__Note__: Regardless of the configuration specified, Secure cookies are only enabled for HTTPS requests.
336+
337+
#### Boolean-based configuration
338+
339+
Boolean-based configuration is intended to globally enable or disable a specific cookie attribute.
340+
341+
```ruby
342+
config.cookies = {
343+
secure: true, # mark all cookies as Secure
344+
httponly: false, # do not mark any cookies as HttpOnly
345+
}
346+
```
347+
348+
#### Hash-based configuration
349+
350+
Hash-based configuration allows for fine-grained control.
351+
352+
```ruby
353+
config.cookies = {
354+
secure: { except: ['_guest'] }, # mark all but the `_guest` cookie as Secure
355+
httponly: { only: ['_rails_session'] }, # only mark the `_rails_session` cookie as HttpOnly
356+
}
357+
```
358+
359+
#### SameSite cookie configuration
360+
361+
SameSite cookies permit either `Strict` or `Lax` enforcement mode options.
362+
363+
```ruby
364+
config.cookies = {
365+
samesite: {
366+
strict: true # mark all cookies as SameSite=Strict
367+
}
368+
}
369+
```
370+
371+
`Strict` and `Lax` enforcement modes can also be specified using a Hash.
372+
373+
```ruby
374+
config.cookies = {
375+
samesite: {
376+
strict: { only: ['_rails_session'] },
377+
lax: { only: ['_guest'] }
378+
}
379+
}
380+
```
381+
325382
### Using with Sinatra
326383

327384
Here's an example using SecureHeaders for Sinatra applications:

lib/secure_headers.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
require "secure_headers/configuration"
22
require "secure_headers/hash_helper"
3+
require "secure_headers/headers/cookie"
34
require "secure_headers/headers/public_key_pins"
45
require "secure_headers/headers/content_security_policy"
56
require "secure_headers/headers/x_frame_options"

lib/secure_headers/configuration.rb

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -104,9 +104,9 @@ def deep_copy_if_hash(value)
104104

105105
attr_writer :hsts, :x_frame_options, :x_content_type_options,
106106
:x_xss_protection, :x_download_options, :x_permitted_cross_domain_policies,
107-
:hpkp, :dynamic_csp, :secure_cookies
107+
:hpkp, :dynamic_csp, :cookies
108108

109-
attr_reader :cached_headers, :csp, :dynamic_csp, :secure_cookies
109+
attr_reader :cached_headers, :csp, :dynamic_csp, :cookies
110110

111111
HASH_CONFIG_FILE = ENV["secure_headers_generated_hashes_file"] || "config/secure_headers_generated_hashes.yml"
112112
if File.exists?(HASH_CONFIG_FILE)
@@ -126,7 +126,7 @@ def initialize(&block)
126126
# Returns a deep-dup'd copy of this configuration.
127127
def dup
128128
copy = self.class.new
129-
copy.secure_cookies = @secure_cookies
129+
copy.cookies = @cookies
130130
copy.csp = self.class.send(:deep_copy_if_hash, @csp)
131131
copy.dynamic_csp = self.class.send(:deep_copy_if_hash, @dynamic_csp)
132132
copy.cached_headers = self.class.send(:deep_copy_if_hash, @cached_headers)
@@ -181,13 +181,19 @@ def validate_config!
181181
XDownloadOptions.validate_config!(@x_download_options)
182182
XPermittedCrossDomainPolicies.validate_config!(@x_permitted_cross_domain_policies)
183183
PublicKeyPins.validate_config!(@hpkp)
184+
Cookie.validate_config!(@cookies)
185+
end
186+
187+
def secure_cookies=(secure_cookies)
188+
Kernel.warn "#{Kernel.caller.first}: [DEPRECATION] `#secure_cookies=` is deprecated. Please use `#cookies=` to configure secure cookies instead."
189+
@cookies = (@cookies || {}).merge(secure: secure_cookies)
184190
end
185191

186192
protected
187193

188194
def csp=(new_csp)
189195
if self.dynamic_csp
190-
raise IllegalPolicyModificationError, "You are attempting to modify CSP settings directly. Use dynamic_csp= isntead."
196+
raise IllegalPolicyModificationError, "You are attempting to modify CSP settings directly. Use dynamic_csp= instead."
191197
end
192198

193199
@csp = new_csp
Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
require 'cgi'
2+
require 'secure_headers/utils/cookies_config'
3+
4+
module SecureHeaders
5+
class CookiesConfigError < StandardError; end
6+
class Cookie
7+
8+
class << self
9+
def validate_config!(config)
10+
CookiesConfig.new(config).validate!
11+
end
12+
end
13+
14+
attr_reader :raw_cookie, :config
15+
16+
def initialize(cookie, config)
17+
@raw_cookie = cookie
18+
@config = config
19+
@attributes = {
20+
httponly: nil,
21+
samesite: nil,
22+
secure: nil,
23+
}
24+
25+
parse(cookie)
26+
end
27+
28+
def to_s
29+
@raw_cookie.dup.tap do |c|
30+
c << "; secure" if secure?
31+
c << "; HttpOnly" if httponly?
32+
c << "; #{samesite_cookie}" if samesite?
33+
end
34+
end
35+
36+
def secure?
37+
flag_cookie?(:secure) && !already_flagged?(:secure)
38+
end
39+
40+
def httponly?
41+
flag_cookie?(:httponly) && !already_flagged?(:httponly)
42+
end
43+
44+
def samesite?
45+
flag_samesite? && !already_flagged?(:samesite)
46+
end
47+
48+
private
49+
50+
def parsed_cookie
51+
@parsed_cookie ||= CGI::Cookie.parse(raw_cookie)
52+
end
53+
54+
def already_flagged?(attribute)
55+
@attributes[attribute]
56+
end
57+
58+
def flag_cookie?(attribute)
59+
case config[attribute]
60+
when TrueClass
61+
true
62+
when Hash
63+
conditionally_flag?(config[attribute])
64+
else
65+
false
66+
end
67+
end
68+
69+
def conditionally_flag?(configuration)
70+
if(Array(configuration[:only]).any? && (Array(configuration[:only]) & parsed_cookie.keys).any?)
71+
true
72+
elsif(Array(configuration[:except]).any? && (Array(configuration[:except]) & parsed_cookie.keys).none?)
73+
true
74+
else
75+
false
76+
end
77+
end
78+
79+
def samesite_cookie
80+
if flag_samesite_lax?
81+
"SameSite=Lax"
82+
elsif flag_samesite_strict?
83+
"SameSite=Strict"
84+
end
85+
end
86+
87+
def flag_samesite?
88+
flag_samesite_lax? || flag_samesite_strict?
89+
end
90+
91+
def flag_samesite_lax?
92+
flag_samesite_enforcement?(:lax)
93+
end
94+
95+
def flag_samesite_strict?
96+
flag_samesite_enforcement?(:strict)
97+
end
98+
99+
def flag_samesite_enforcement?(mode)
100+
return unless config[:samesite]
101+
102+
case config[:samesite][mode]
103+
when Hash
104+
conditionally_flag?(config[:samesite][mode])
105+
when TrueClass
106+
true
107+
else
108+
false
109+
end
110+
end
111+
112+
def parse(cookie)
113+
return unless cookie
114+
115+
cookie.split(/[;,]\s?/).each do |pairs|
116+
name, values = pairs.split('=',2)
117+
name = CGI.unescape(name)
118+
119+
attribute = name.downcase.to_sym
120+
if @attributes.has_key?(attribute)
121+
@attributes[attribute] = values || true
122+
end
123+
end
124+
end
125+
end
126+
end

lib/secure_headers/middleware.rb

Lines changed: 23 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,5 @@
11
module SecureHeaders
22
class Middleware
3-
SECURE_COOKIE_REGEXP = /;\s*secure\s*(;|$)/i.freeze
4-
53
def initialize(app)
64
@app = app
75
end
@@ -12,27 +10,43 @@ def call(env)
1210
status, headers, response = @app.call(env)
1311

1412
config = SecureHeaders.config_for(req)
15-
flag_cookies_as_secure!(headers) if config.secure_cookies
13+
flag_cookies!(headers, override_secure(env, config.cookies)) if config.cookies
1614
headers.merge!(SecureHeaders.header_hash_for(req))
1715
[status, headers, response]
1816
end
1917

2018
private
2119

2220
# inspired by https://github.com/tobmatth/rack-ssl-enforcer/blob/6c014/lib/rack/ssl-enforcer.rb#L183-L194
23-
def flag_cookies_as_secure!(headers)
21+
def flag_cookies!(headers, config)
2422
if cookies = headers['Set-Cookie']
2523
# Support Rails 2.3 / Rack 1.1 arrays as headers
2624
cookies = cookies.split("\n") unless cookies.is_a?(Array)
2725

2826
headers['Set-Cookie'] = cookies.map do |cookie|
29-
if cookie !~ SECURE_COOKIE_REGEXP
30-
"#{cookie}; secure"
31-
else
32-
cookie
33-
end
27+
SecureHeaders::Cookie.new(cookie, config).to_s
3428
end.join("\n")
3529
end
3630
end
31+
32+
# disable Secure cookies for non-https requests
33+
def override_secure(env, config = {})
34+
if scheme(env) != 'https'
35+
config.merge!(secure: false)
36+
end
37+
38+
config
39+
end
40+
41+
# derived from https://github.com/tobmatth/rack-ssl-enforcer/blob/6c014/lib/rack/ssl-enforcer.rb#L119
42+
def scheme(env)
43+
if env['HTTPS'] == 'on' || env['HTTP_X_SSL_REQUEST'] == 'on'
44+
'https'
45+
elsif env['HTTP_X_FORWARDED_PROTO']
46+
env['HTTP_X_FORWARDED_PROTO'].split(',')[0]
47+
else
48+
env['rack.url_scheme']
49+
end
50+
end
3751
end
3852
end

0 commit comments

Comments
 (0)