safe_cookies 0.1.5 → 0.2.2

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,15 +1,7 @@
1
1
  ---
2
- !binary "U0hBMQ==":
3
- metadata.gz: !binary |-
4
- MjI5NWM4ZWE1M2UzNWFjNjFkNzc2M2EzN2U4NzJhZTgzNjQ0NmQ1NQ==
5
- data.tar.gz: !binary |-
6
- OGNlMjkzMmI2MTFhZTFlNzA3MDZmMmYxOWQ3YmQ1ZGE4Njg0OWZlOA==
2
+ SHA256:
3
+ metadata.gz: 7175011cd4a253c98779e5fdb5d221ea99dbbe5708e3a9dce37547ac89d29361
4
+ data.tar.gz: 08e2daf10fdd7ec2a115162969eaffd241edca9f82671a146bfafd381bd9f7a3
7
5
  SHA512:
8
- metadata.gz: !binary |-
9
- MjU5MGUyMzQyNTI2MzE5OTkzYjBkOTI4NjQ1ZjQxOTk4NGQzOTkxMDEyNDk3
10
- YmQ3YjJjZDBhYTU3ZjBhNzgzMGYwNzdmNzcyZTFmYWY1NzQ0N2RhZDUyYzA1
11
- NjJlZDc4NjdiMWU2M2NhOWY5YzBmNWFkNWNmY2Y1OTgxNjI1YjQ=
12
- data.tar.gz: !binary |-
13
- Yjg4YTA3OGYyY2RhYmJkZTAyYmNiNjE2ZGRjNTc4MTA0NDJjMTc1ZjA3Mjcy
14
- MjFhYjlhOGY1ZjQyYTE3OTEzODE3ZmU3YmE5YzIxOTNhOWE4NTg5M2M3ZTk4
15
- ZWMzMzVmODk4MWE3MDdjN2RkZjI1N2UwN2EyOTVlNmM0MmMwMjk=
6
+ metadata.gz: 8f9932ab978f4cdeabc001314496ffed4214f9a966b45847e246c2c78730b1388ee1169a4def1864edb78481fbb30cefbffe29fd366f1a3abe207d107175c681
7
+ data.tar.gz: d42dbf95c6d38e99c1e0f356f31925ef6da59ba465a768ad5d25f573a02bb75e671330dbe6dfc4a52bf039879eab7ee53fe371d50d7e540ddaa5f87cd93aa8f6
data/.gitignore CHANGED
@@ -15,3 +15,4 @@ spec/reports
15
15
  test/tmp
16
16
  test/version_tmp
17
17
  tmp
18
+ .idea
data/README.md CHANGED
@@ -1,89 +1,127 @@
1
+ # This gem is no longer maintained!
2
+
3
+ Read about [reasons and alternatives](https://makandracards.com/makandra/53693-rails-making-all-cookies-secure-to-pass-a-security-audit).
4
+
5
+
6
+ --------------------
7
+
1
8
  # SafeCookies
2
9
 
3
- This Gem brings a middleware that will make all cookies secure. In detail, it will:
10
+ This gem has a middleware that will make all cookies secure, by setting the
11
+ `HttpOnly` and the `secure` flag for all cookies the application sets on the
12
+ client.
13
+
14
+ Making a cookie `HttpOnly` prevents Javascripts from seeing it, which really
15
+ should be the default. It makes it way harder to steal cookie information via
16
+ malicious Javascript.
17
+
18
+ Making a cookie `secure` tells the browser to only send the cookie over HTTPS
19
+ connections, protecting it from being sniffed by a man-in-the-middle. (Setting a
20
+ [HSTS](http://en.wikipedia.org/wiki/HTTP_Strict_Transport_Security) header
21
+ achieves the same, but Safari < 7 and IE < 11 don't speak HSTS.)
22
+
23
+ SafeCookies will *additionally* rewrite all cookies the user is sending. **But**
24
+ it can only do so, if the cookie was registered before (see below). It will rewrite
25
+ user cookies only once per user.
4
26
 
5
- * set all new application cookies 'HttpOnly', unless specified otherwise
6
- * set all new application cookies 'secure', if the request came via HTTPS and not specified otherwise
7
- * rewrite request cookies, setting both flags as above
8
27
 
9
28
  ## Installation
10
29
 
11
- Add this line to your application's Gemfile:
30
+ 1. Add `gem 'safe_cookies'` to your application's Gemfile, then run `bundle install`.
12
31
 
13
- gem 'safe_cookies'
32
+ 2. Add a configuration block in an initializer (e.g. `config/initializers/safe_cookies.rb`):
14
33
 
15
- Then run:
34
+ SafeCookies.configure do |config|
35
+ # configuration ...
36
+ end
16
37
 
17
- $ bundle
38
+ 3. Register the middleware:
18
39
 
19
- Or install it yourself as:
40
+ Rails 3+: add the following lines to the application block in `config/application.rb`:
20
41
 
21
- $ gem install safe_cookies
42
+ require 'safe_cookies'
43
+ config.middleware.insert_before ActionDispatch::Cookies, SafeCookies::Middleware
22
44
 
45
+ Rails 2: add the following lines to the initializer block in `config/environment.rb`:
23
46
 
24
- ## Usage
47
+ require 'safe_cookies'
48
+ config.middleware.insert_before ActionController::Session::CookieStore, SafeCookies::Middleware
25
49
 
26
- ### Step 1
27
- **Rails 3**: add the following line in config/application.rb:
50
+ Now all new cookies will be made `secure` and `HttpOnly`. But what about cookies
51
+ already out there?
28
52
 
29
- class Application < Rails::Application
30
- # ...
31
- config.middleware.insert_before ActionDispatch::Cookies, SafeCookies::Middleware
32
- end
33
53
 
34
- **Rails 2:** add the following lines in config/environment.rb:
54
+ ## Updating existing cookies
55
+
56
+ Unfortunately, [the client won't ever tell us](http://tools.ietf.org/html/rfc6265#section-4.2.2)
57
+ if it stores a cookie with flags such as `secure` or which expiry date it is
58
+ stored with. Therefore, in order to make the middleware retroactively secure
59
+ cookies owned by the client, you need to register each of those cookies with
60
+ the middleware, specifying their properties.
35
61
 
36
- Rails::Initializer.run do |config|
37
- # ...
38
- require 'safe_cookies'
39
- config.middleware.insert_before ActionController::Session::CookieStore, SafeCookies::Middleware
62
+ Carefully scan your app for cookies you are using. There's no easy way to find
63
+ out if you missed one (but see below for some help the gem provides).
64
+
65
+ SafeCookies.configure do |config|
66
+ config.register_cookie 'remember_token', :expire_after => 1.year
67
+ config.register_cookie 'last_action', :expire_after => 30.days, :path => '/commerce'
40
68
  end
41
69
 
42
- ### Step 2
43
- Register cookies, either just after the lines you added in step 1 or in in an initializer
44
- (e.g. config/initializers/safe_cookies.rb):
70
+ Available options are: `:expire_after` (required)`, :path, :secure, :http_only`.
71
+ For cookies with "session" expiry, set `:expire_after => nil`.
72
+
73
+
74
+ ## Having a cookie non-secure or non-HttpOnly
75
+
76
+ Tell SafeCookies which cookies not to make `secure` or `HttpOnly` by registering
77
+ them, just like above:
45
78
 
46
79
  SafeCookies.configure do |config|
47
- config.register_cookie :remember_token, :expire_after => 1.year
48
- config.register_cookie :last_action, :expire_after => 30.days
49
- config.register_cookie :default_language, :expire_after => 10.years, :secure => false
50
- config.register_cookie :javascript_data, :expire_after => 1.day, :http_only => false
80
+ config.register_cookie 'default_language', :expire_after => 10.years, :secure => false
81
+ config.register_cookie 'javascript_data', :expire_after => 1.day, :http_only => false
51
82
  end
52
83
 
53
- This will have the `default_language` cookie not made secure, the `javascript_data` cookie
54
- not made http-only. It will rewrite the `remember_token` with an expiry of one year and the
55
- `last_action` cookie with an expiry of 30 days, making both of them secure and http-only.
56
- Available options are: `:expire_after (required), :path, :secure, :http_only`.
57
84
 
58
- ### Step 3
59
- Override `SafeCookies::Middleware#handle_unknown_cookies(cookies)` (see "Dealing with unregistered cookies" below).
85
+ ## Finding unregistered user cookies
86
+
87
+ There are lots of cookies your application receives that you never did set.
88
+ However, if you want to know about any unknown cookies touching your
89
+ application, SafeCookies gives you two tools.
90
+
91
+ 1) If you set `config.log_unknown_cookies = true` in the configuration block, all
92
+ unknown cookies will be written to the Rails log. When you start implementing
93
+ the middleware, closely watch it to find cookies you forgot to register.
94
+
95
+ 2) You may overwrite `SafeCookies::Middleware#handle_unknown_cookies(cookies)`
96
+ in the configuration block for customized behaviour (like, notifying you per
97
+ email).
98
+
99
+ To ignore cookies that are irrelevant to you, you may configure them to be
100
+ ignored. Use the `config.ignore_cookie` directive, which takes either a String
101
+ or a Regex parameter. *Be careful when using regular expressions!*
60
102
 
61
103
 
62
- ## Dealing with unregistered cookies
104
+ ## Fixing cookie paths
63
105
 
64
- The middleware is not able to secure cookies without knowing their properties (most important: their
65
- expiry). Unfortunately, the [client won't ever tell us](http://tools.ietf.org/html/rfc6265#section-4.2.2)
66
- if the cookie was originally sent with flags such as "secure" or which expiry date it currently has.
67
- Therefore, it is important to register all cookies that users may come with, specifying their properties.
68
- Unregistered cookies cannot be secured.
106
+ In August 2013 we noticed a bug in SafeCookies < 0.1.4, by which secured cookies
107
+ would be set for the current "directory" (see comments in `cookie_path_fix.rb`)
108
+ instead of root (which usually is what you want). Users would get multiple
109
+ cookies for that domain, leading to issues like being unable to sign in.
69
110
 
70
- If a request brings a cookie that is not registered, the middleware will raise
71
- `SafeCookies::UnknownCookieError`. Rails 3+ should handle the exception as any other in your application,
72
- but by default, **you will not be notified from Rails 2 applications** and the user will see a standard
73
- 500 Server Error. Override `SafeCookies::Middleware#handle_unknown_cookies(cookies)` in the config
74
- initializer for customized exception handling (like, notifying you per email).
111
+ The configuration option `config.fix_paths` turns on fixing this error. It
112
+ expects an option `:for_cookies_secured_before => Time.parse('some minutes after
113
+ you will have deployed')` which reflects the point of time from which SafeCookies
114
+ can expect cookies to be set with the correct path. It will only rewrite cookies
115
+ with a new path if it had set them before that point of time.
75
116
 
76
- You should not ignore an unregistered cookie, but instead register it.
77
117
 
118
+ ## Development
78
119
 
79
- ## Fix cookie paths
120
+ - Tests live in `spec`.
121
+ - You can run specs from the project root by saying `bundle exec rake`.
80
122
 
81
- In August 2013 we noticed a bug in SafeCookies < 0.1.4, by which secured cookies would be set for the
82
- current "directory" (see comments in `cookie_path_fix.rb`) instead of root (which usually is what you want).
83
- Users would get multiple cookies for that domain, leading to issues like being unable to sign in.
123
+ If you would like to contribute:
84
124
 
85
- The configuration option `config.fix_paths` turns on fixing this error. It requires an option
86
- `:for_cookies_secured_before => Time.parse('some minutes after you will have deployed')` which reflects the
87
- point of time from which cookies will be secured with the correct path. The middleware will fix the cookie
88
- paths by rewriting all cookies that it has already secured, but only if the were secured before the time
89
- you specified.
125
+ - Fork the repository.
126
+ - Push your changes **with passing specs**.
127
+ - Send us a pull request.
data/lib/safe_cookies.rb CHANGED
@@ -5,12 +5,11 @@ require "safe_cookies/helpers"
5
5
  require "safe_cookies/util"
6
6
  require "safe_cookies/version"
7
7
  require "rack"
8
+ require "time" # add Time#rfc2822
8
9
 
9
10
  # Naming:
10
11
  # - application_cookies: cookies received from the application. The 'Set-Cookie' header is a string
11
12
  # - request_cookies: cookies received from the client. Rack::Request#cookies returns a Hash of { 'name' => 'value' }
12
- # - response_cookies: cookies to be sent to the client
13
- # (= application_cookies + any cookies set in the middleware)
14
13
 
15
14
  module SafeCookies
16
15
 
@@ -19,6 +18,7 @@ module SafeCookies
19
18
  STORE_COOKIE_NAME = '_safe_cookies__known_cookies'
20
19
  SECURED_COOKIE_NAME = 'secured_old_cookies'
21
20
  HELPER_COOKIES_LIFETIME = 10 * 365 * 24 * 60 * 60 # 10 years
21
+
22
22
 
23
23
  class Middleware
24
24
 
@@ -30,22 +30,23 @@ module SafeCookies
30
30
 
31
31
  def initialize(app)
32
32
  @app = app
33
- @configuration = SafeCookies.configuration or raise "Don't know what to do without configuration"
33
+ @config = SafeCookies.configuration or raise "Don't know what to do without configuration"
34
34
  end
35
35
 
36
36
  def call(env)
37
37
  reset_instance_variables
38
38
 
39
39
  @request = Rack::Request.new(env)
40
- ensure_no_unknown_cookies_in_request!
40
+ check_if_request_has_unknown_cookies
41
41
 
42
+ # call the next middleware up the stack
42
43
  status, @headers, body = @app.call(env)
43
44
  cache_application_cookies_string
44
45
 
45
- remove_application_cookies_from_request_cookies
46
- rewrite_application_cookies
46
+ enhance_application_cookies!
47
47
  store_application_cookie_names
48
- fix_cookie_paths if fix_cookie_paths?
48
+
49
+ delete_cookies_on_bad_path if fix_cookie_paths?
49
50
  rewrite_request_cookies unless cookies_have_been_rewritten_before?
50
51
 
51
52
  [ status, @headers, body ]
@@ -53,31 +54,27 @@ module SafeCookies
53
54
 
54
55
  private
55
56
 
57
+ # Instance variables survive requests because the middleware is a singleton.
56
58
  def reset_instance_variables
57
- @request, @headers, @application_cookies = nil
59
+ @request, @headers, @application_cookies_string = nil
58
60
  end
59
61
 
60
- def ensure_no_unknown_cookies_in_request!
62
+ def check_if_request_has_unknown_cookies
61
63
  request_cookie_names = request_cookies.keys.map(&:to_s)
62
64
  unknown_cookie_names = request_cookie_names - known_cookie_names
63
65
 
64
66
  if unknown_cookie_names.any?
65
- handle_unknown_cookies(unknown_cookie_names)
66
- end
67
- end
67
+ message = "Request for '#{@request.url}' had unknown cookies: #{unknown_cookie_names.join(', ')}"
68
+ log(message) if @config.log_unknown_cookies
68
69
 
69
- def remove_application_cookies_from_request_cookies
70
- if @application_cookies
71
- application_cookie_names = @application_cookies.scan(COOKIE_NAME_REGEX)
72
- application_cookie_names.each do |cookie|
73
- request_cookies.delete(cookie)
74
- end
70
+ handle_unknown_cookies(unknown_cookie_names)
75
71
  end
76
72
  end
77
-
78
- def rewrite_application_cookies
79
- if @application_cookies
80
- cookies = @application_cookies.split("\n")
73
+
74
+ # Overwrites @header['Set-Cookie']!
75
+ def enhance_application_cookies!
76
+ if @application_cookies_string
77
+ cookies = @application_cookies_string.split("\n")
81
78
 
82
79
  # On Rack 1.1, cookie values sometimes contain trailing newlines.
83
80
  # Example => ["foo=1; path=/\n", "bar=2; path=/"]
@@ -96,36 +93,51 @@ module SafeCookies
96
93
  @headers['Set-Cookie'] = cookies.join("\n")
97
94
  end
98
95
  end
99
-
96
+
97
+ # Store the names of cookies that are set by the application.
98
+ # (We are already securing those and will not need to rewrite them.)
100
99
  def store_application_cookie_names
101
- if @application_cookies
102
- application_cookie_names = stored_application_cookie_names + @application_cookies.scan(COOKIE_NAME_REGEX)
100
+ if @application_cookies_string
101
+ application_cookie_names = stored_application_cookie_names + @application_cookies_string.scan(COOKIE_NAME_REGEX)
103
102
  application_cookies_string = application_cookie_names.uniq.join(KNOWN_COOKIES_DIVIDER)
104
103
 
105
104
  set_cookie!(STORE_COOKIE_NAME, application_cookies_string, :expire_after => HELPER_COOKIES_LIFETIME)
106
105
  end
107
106
  end
108
107
 
109
- # This method takes all cookies sent with the request and rewrites them,
108
+ # This method takes the cookies sent with the request and rewrites them,
110
109
  # making them both secure and http-only (unless specified otherwise in
111
110
  # the configuration).
112
111
  # With the SECURED_COOKIE_NAME cookie we remember the exact time that we
113
112
  # rewrote the cookies.
114
113
  def rewrite_request_cookies
115
- if request_cookies.any?
116
- registered_cookies_in_request.each do |cookie_name, options|
117
- value = request_cookies[cookie_name]
118
-
114
+ rewritable_cookies = rewritable_request_cookies
115
+
116
+ # don't rewrite request cookies that the application is setting in the response
117
+ if @application_cookies_string
118
+ app_cookie_names = @application_cookies_string.scan(COOKIE_NAME_REGEX)
119
+ Util.except!(rewritable_cookies, *app_cookie_names)
120
+ end
121
+
122
+ if rewritable_cookies.any?
123
+ rewritable_cookies.each do |cookie_name, value|
124
+ options = @config.registered_cookies[cookie_name]
119
125
  set_cookie!(cookie_name, value, options)
120
126
  end
121
127
 
122
- formatted_now = Rack::Utils.rfc2822(Time.now.gmtime)
123
- set_cookie!(SECURED_COOKIE_NAME, formatted_now, :expire_after => HELPER_COOKIES_LIFETIME)
128
+ set_cookie!(SECURED_COOKIE_NAME, Time.now.gmtime.rfc2822, :expire_after => HELPER_COOKIES_LIFETIME)
124
129
  end
125
130
  end
126
131
 
132
+ # API method
127
133
  def handle_unknown_cookies(cookie_names)
128
- raise SafeCookies::UnknownCookieError.new("Request for '#{@request.url}' had unknown cookies: #{cookie_names.join(', ')}")
134
+ end
135
+
136
+ def log(error_message)
137
+ message = '** [SafeCookies] '
138
+ message << error_message
139
+
140
+ Rails.logger.error(message) if defined?(Rails)
129
141
  end
130
142
 
131
143
  end
@@ -13,12 +13,15 @@ module SafeCookies
13
13
  end
14
14
 
15
15
  class Configuration
16
- attr_reader :registered_cookies, :fix_cookie_paths, :correct_cookie_paths_timestamp
16
+ attr_accessor :log_unknown_cookies
17
+ attr_reader :registered_cookies, :fix_cookie_paths, :correct_cookie_paths_timestamp,
18
+ :ignored_cookies
17
19
 
18
20
  def initialize
19
21
  self.registered_cookies = {}
20
22
  self.insecure_cookies = []
21
23
  self.scriptable_cookies = []
24
+ self.ignored_cookies = []
22
25
  end
23
26
 
24
27
  # Register cookies you expect to receive. The middleware will rewrite all
@@ -45,6 +48,14 @@ module SafeCookies
45
48
  scriptable_cookies << name if options[:http_only] == false
46
49
  end
47
50
 
51
+ # Ignore cookies that you don't control like this:
52
+ #
53
+ # ignore_cookie 'ignored_cookie'
54
+ # ignore_cookie /^__utm/
55
+ def ignore_cookie(name_or_regex)
56
+ self.ignored_cookies << name_or_regex
57
+ end
58
+
48
59
  def fix_paths(options = {})
49
60
  options.has_key?(:for_cookies_secured_before) or raise MissingOptionError.new("Was told to fix paths without the :for_cookies_secured_before timestamp.")
50
61
 
@@ -63,7 +74,8 @@ module SafeCookies
63
74
  private
64
75
 
65
76
  attr_accessor :insecure_cookies, :scriptable_cookies
66
- attr_writer :registered_cookies, :fix_cookie_paths, :correct_cookie_paths_timestamp
77
+ attr_writer :registered_cookies, :fix_cookie_paths, :correct_cookie_paths_timestamp,
78
+ :ignored_cookies
67
79
 
68
80
  end
69
81
 
@@ -2,39 +2,37 @@ module SafeCookies
2
2
  module CookiePathFix
3
3
 
4
4
  # Previously, the SafeCookies gem would not set a path when rewriting
5
- # cookies. Browsers then would assume and store the current "directory",
6
- # leading to multiple cookies per domain.
5
+ # cookies. Browsers then would assume and store the current "directory"
6
+ # (see below), leading to multiple cookies per domain.
7
+ #
8
+ # If the cookies were secured before the configured datetime, this method
9
+ # instructs the client to delete all cookies it sent with the request and
10
+ # that we are able to rewrite, plus the SECURED_COOKIE_NAME helper cookie.
7
11
  #
8
- # If cookies had been secured before the configured datetime, the method
9
- # `fix_cookie_paths` deletes all cookies coming with the request, and the
10
- # SECURED_COOKIE_NAME helper cookie.
11
12
  # The middleware still sees the request cookies and will rewrite them as
12
- # if it hadn't seen them before.
13
-
14
- def fix_cookie_paths
15
- registered_cookies_in_request.keys.each do |registered_cookie|
16
- delete_cookie_for_current_directory(registered_cookie)
17
- end
13
+ # if it hadn't seen them before, setting them on the correct path (root,
14
+ # by default).
15
+ def delete_cookies_on_bad_path
16
+ rewritable_request_cookies.keys.each &method(:delete_cookie_for_current_directory)
18
17
  delete_cookie_for_current_directory(SafeCookies::SECURED_COOKIE_NAME)
19
18
 
20
- # Delete this cookie here, so the middleware will secure all cookies anew.
21
- request_cookies.delete(SafeCookies::SECURED_COOKIE_NAME)
19
+ # Delete this cookie here, so the middleware believes it hasn't secured
20
+ # the cookies yet.
21
+ @request.cookies.delete(SafeCookies::SECURED_COOKIE_NAME)
22
22
  end
23
23
 
24
24
  private
25
25
 
26
26
  def fix_cookie_paths?
27
- @configuration.fix_cookie_paths or return false
28
-
29
- cookies_need_path_fix = (secured_old_cookies_timestamp < @configuration.correct_cookie_paths_timestamp)
30
-
31
- cookies_have_been_rewritten_before? and cookies_need_path_fix
27
+ @config.fix_cookie_paths &&
28
+ cookies_have_been_rewritten_before? &&
29
+ (secured_old_cookies_timestamp < @config.correct_cookie_paths_timestamp)
32
30
  end
33
31
 
34
32
  # Delete cookies by giving them an expiry in the past,
35
33
  # cf. https://tools.ietf.org/html/rfc6265#section-4.1.2.
36
34
  #
37
- # Most important, as specified in
35
+ # Most importantly, as specified in
38
36
  # https://tools.ietf.org/html/rfc6265#section-4.1.2.4 and in section 5.1.4,
39
37
  # cookies set without a path will be set for the current "directory", that is:
40
38
  #
@@ -51,16 +49,23 @@ module SafeCookies
51
49
  end
52
50
 
53
51
  def current_directory_is_root?
54
- !@request.path[%r(^/[^/]+/[^\?]+), 0] # roughly: "there are not three slashes"
52
+ # in words: "there are not three slashes before any query params"
53
+ !@request.path[%r(^/[^/]+/[^\?]+), 0]
55
54
  end
56
55
 
57
56
  def secured_old_cookies_timestamp
58
- Time.rfc2822(request_cookies[SafeCookies::SECURED_COOKIE_NAME])
57
+ @request.cookies.has_key?(SafeCookies::SECURED_COOKIE_NAME) or return nil
58
+
59
+ Time.rfc2822(@request.cookies[SafeCookies::SECURED_COOKIE_NAME])
59
60
  rescue ArgumentError
60
61
  # If we cannot parse the secured_old_cookies time,
61
62
  # assume it was before we noticed the bug to ensure
62
63
  # broken cookie paths will be fixed.
63
- Time.parse "2013-08-25 0:00"
64
+ #
65
+ # One reason to get here is that Rack::Utils.rfc2822 produces an invalid
66
+ # datetime string in Rack v1.1, writing the date with dashes
67
+ # (e.g. '04-Nov-2013').
68
+ Time.parse "2013-08-25 00:00"
64
69
  end
65
70
 
66
71
  end
@@ -3,14 +3,18 @@ module SafeCookies
3
3
 
4
4
  KNOWN_COOKIES_DIVIDER = '|'
5
5
 
6
+ # Since we have to operate on and modify the actual @headers hash that the
7
+ # application returns, cache the @headers['Set-Cookie'] string so that
8
+ # later on, we still know what the application did set.
6
9
  def cache_application_cookies_string
7
10
  cookies = @headers['Set-Cookie']
8
11
  # Rack 1.1 returns an Array
9
12
  cookies = cookies.join("\n") if cookies.is_a?(Array)
10
13
 
11
14
  if cookies and cookies.length > 0
12
- @application_cookies = cookies
15
+ @application_cookies_string = cookies
13
16
  end
17
+ # else, @application_cookies_string will be `nil`
14
18
  end
15
19
 
16
20
  def secure(cookie)
@@ -40,42 +44,43 @@ module SafeCookies
40
44
  options[:secure] = should_be_secure?(name)
41
45
  options[:httponly] = should_be_http_only?(name)
42
46
 
47
+ # Rack magic
43
48
  Rack::Utils.set_cookie_header!(@headers, name, options)
44
49
  end
45
50
 
46
51
 
47
52
  # getters
53
+
54
+ # returns the request cookies minus ignored cookies
55
+ def request_cookies
56
+ Util.except!(@request.cookies.dup, *@config.ignored_cookies)
57
+ end
48
58
 
49
59
  def stored_application_cookie_names
50
- store_cookie = request_cookies[STORE_COOKIE_NAME] || ""
60
+ store_cookie = @request.cookies[STORE_COOKIE_NAME] || ""
51
61
  store_cookie.split(KNOWN_COOKIES_DIVIDER)
52
62
  end
53
63
 
54
- # returns those of the registered cookies that appear in the request
55
- def registered_cookies_in_request
56
- Util.slice(@configuration.registered_cookies, *request_cookies.keys)
64
+ def rewritable_request_cookies
65
+ Util.slice(request_cookies, *@config.registered_cookies.keys)
57
66
  end
58
67
 
59
68
  def known_cookie_names
60
69
  known = [STORE_COOKIE_NAME, SECURED_COOKIE_NAME]
61
70
  known += stored_application_cookie_names
62
- known += @configuration.registered_cookies.keys
63
- end
64
-
65
- def request_cookies
66
- @request.cookies
71
+ known += @config.registered_cookies.keys
67
72
  end
68
73
 
69
74
 
70
75
  # boolean
71
76
 
72
77
  def cookies_have_been_rewritten_before?
73
- request_cookies.has_key? SECURED_COOKIE_NAME
78
+ @request.cookies.has_key? SECURED_COOKIE_NAME
74
79
  end
75
80
 
76
81
  def should_be_secure?(cookie)
77
82
  cookie_name = cookie.split('=').first.strip
78
- ssl? and not @configuration.insecure_cookie?(cookie_name)
83
+ ssl? and not @config.insecure_cookie?(cookie_name)
79
84
  end
80
85
 
81
86
  def ssl?
@@ -89,7 +94,7 @@ module SafeCookies
89
94
 
90
95
  def should_be_http_only?(cookie)
91
96
  cookie_name = cookie.split('=').first.strip
92
- not @configuration.scriptable_cookie?(cookie_name)
97
+ not @config.scriptable_cookie?(cookie_name)
93
98
  end
94
99
 
95
100
  end
@@ -1,17 +1,22 @@
1
- module SafeCookies
2
- class Util
3
- class << self
4
-
5
- def slice(hash, *allowed_keys)
6
- sliced_hash = hash.select { |key, value|
7
- allowed_keys.include? key
8
- }
1
+ class SafeCookies::Util
2
+ class << self
3
+
4
+ def slice(hash, *allowed_keys)
5
+ sliced_hash = hash.select { |key, _value|
6
+ allowed_keys.include? key
7
+ }
9
8
 
10
- # Normalize the result of Hash#select
11
- # (Ruby 1.8 returns an Array, Ruby 1.9 returns a Hash)
12
- Hash[sliced_hash]
9
+ # Normalize the result of Hash#select
10
+ # (Ruby 1.8 returns an Array, Ruby 1.9 returns a Hash)
11
+ Hash[sliced_hash]
12
+ end
13
+
14
+ # rejected_keys may be of type String or Regex
15
+ def except!(hash, *rejected_keys)
16
+ hash.delete_if do |key, _value|
17
+ rejected_keys.any? { |rejected| rejected === key }
13
18
  end
14
-
15
19
  end
20
+
16
21
  end
17
- end
22
+ end
@@ -1,3 +1,3 @@
1
1
  module SafeCookies
2
- VERSION = '0.1.5'
2
+ VERSION = '0.2.2'
3
3
  end
data/safe_cookies.gemspec CHANGED
@@ -7,6 +7,7 @@ Gem::Specification.new do |gem|
7
7
  gem.description = %q{Make all cookies `secure` and `HttpOnly`.}
8
8
  gem.summary = %q{Make all cookies `secure` and `HttpOnly`.}
9
9
  gem.homepage = "http://www.makandra.de"
10
+ gem.license = "MIT"
10
11
 
11
12
  gem.files = `git ls-files`.split($\)
12
13
  gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
@@ -3,7 +3,7 @@ require 'cgi'
3
3
 
4
4
  describe SafeCookies::Middleware do
5
5
 
6
- describe 'cookie path fix' do
6
+ describe 'cookie path fix,' do
7
7
 
8
8
  subject { described_class.new(app) }
9
9
  let(:app) { stub 'application' }
@@ -15,12 +15,11 @@ describe SafeCookies::Middleware do
15
15
  end
16
16
 
17
17
  def set_default_request_cookies(secured_at = Time.parse('2040-01-01 00:00'))
18
- secured_old_cookies_time = Rack::Utils.rfc2822(secured_at.gmtime)
19
- set_request_cookies(env, 'cookie_to_update=some_data', "secured_old_cookies=#{CGI::escape(secured_old_cookies_time)}")
18
+ set_request_cookies(env, 'cookie_to_update=some_data', "secured_old_cookies=#{CGI::escape(secured_at.gmtime.rfc2822)}")
20
19
  end
21
20
 
22
21
 
23
- context 'rewriting previously secured cookies' do
22
+ context 'rewriting previously secured cookies,' do
24
23
 
25
24
  before do
26
25
  SafeCookies.configure do |config|
@@ -109,11 +108,21 @@ describe SafeCookies::Middleware do
109
108
  headers['Set-Cookie'].should =~ %r(secured_old_cookies=\w+.*path=/;)
110
109
  end
111
110
 
112
- it 'rewrites cookies even if it cannot parse the secured_old_cookies timestamp' do
113
- set_request_cookies(env, 'cookie_to_update=some_data', 'secured_old_cookies=rubbish')
114
-
115
- code, headers, response = subject.call(env)
116
- headers['Set-Cookie'].should =~ /cookie_to_update=some_data;.*path=\/;/
111
+ context 'unparseable secured_old_cookies timestamp,' do
112
+
113
+ before do
114
+ set_request_cookies(env, 'cookie_to_update=some_data', 'secured_old_cookies=rubbish')
115
+ code, @headers, response = subject.call(env)
116
+ end
117
+
118
+ it 'rewrites cookies anyway' do
119
+ @headers['Set-Cookie'].should include('cookie_to_update=some_data;')
120
+ end
121
+
122
+ it 'sets a new, parseable secured_old_cookies timestamp' do
123
+ @headers['Set-Cookie'].should include("secured_old_cookies=#{CGI::escape @now.gmtime.rfc2822}")
124
+ end
125
+
117
126
  end
118
127
 
119
128
  end
@@ -7,7 +7,7 @@ describe SafeCookies::Middleware do
7
7
  let(:app) { stub 'application' }
8
8
  let(:env) { { 'HTTPS' => 'on' } }
9
9
 
10
- it 'should rewrite registered request cookies as secure and http-only, but only once' do
10
+ it 'rewrites registered request cookies as secure and http-only, but only once' do
11
11
  SafeCookies.configure do |config|
12
12
  config.register_cookie('foo', :expire_after => 3600)
13
13
  end
@@ -35,7 +35,7 @@ describe SafeCookies::Middleware do
35
35
  headers['Set-Cookie'].to_s.should == ''
36
36
  end
37
37
 
38
- it 'should not make cookies secure if the request was not secure' do
38
+ it 'doesn’t make cookies secure if the request was not secure' do
39
39
  stub_app_call(app, :application_cookies => 'filter-settings=sort_by_date')
40
40
  env['HTTPS'] = 'off'
41
41
 
@@ -43,7 +43,7 @@ describe SafeCookies::Middleware do
43
43
  headers['Set-Cookie'].should include("filter-settings=sort_by_date")
44
44
  headers['Set-Cookie'].should_not match(/\bsecure\b/i)
45
45
  end
46
-
46
+
47
47
  it 'expires the secured_old_cookies helper cookie in ten years' do
48
48
  Timecop.freeze(Time.parse('2013-09-17 17:53'))
49
49
 
@@ -59,43 +59,47 @@ describe SafeCookies::Middleware do
59
59
  headers['Set-Cookie'].should =~ /secured_old_cookies.*expires=Fri, 15 Sep 2023 \d\d:\d\d:\d\d/
60
60
  end
61
61
 
62
- it 'sets cookies on the root path' do
63
- SafeCookies.configure do |config|
64
- config.register_cookie('my_old_cookie', :expire_after => 3600)
65
- end
62
+ context 'cookie attributes' do
63
+
64
+ it 'sets cookies on the root path' do
65
+ SafeCookies.configure do |config|
66
+ config.register_cookie('my_old_cookie', :expire_after => 3600)
67
+ end
66
68
 
67
- set_request_cookies(env, 'my_old_cookie=foobar')
68
- stub_app_call(app)
69
+ set_request_cookies(env, 'my_old_cookie=foobar')
70
+ stub_app_call(app)
69
71
 
70
- code, headers, response = subject.call(env)
72
+ code, headers, response = subject.call(env)
71
73
 
72
- cookies = headers['Set-Cookie'].split("\n")
73
- cookies.each do |cookie|
74
- cookie.should include('; path=/;')
74
+ cookies = headers['Set-Cookie'].split("\n")
75
+ cookies.each do |cookie|
76
+ cookie.should include('; path=/;')
77
+ end
75
78
  end
76
- end
77
79
 
78
- it 'should not alter cookie options coming from the application' do
79
- stub_app_call(app, :application_cookies => 'cookie=data; path=/; expires=next_week')
80
+ it 'should not alter cookie attributes coming from the application' do
81
+ stub_app_call(app, :application_cookies => 'cookie=data; path=/; expires=next_week')
80
82
 
81
- code, headers, response = subject.call(env)
82
- headers['Set-Cookie'].should =~ %r(cookie=data; path=/; expires=next_week; secure; HttpOnly)
83
- end
83
+ code, headers, response = subject.call(env)
84
+ headers['Set-Cookie'].should =~ %r(cookie=data; path=/; expires=next_week; secure; HttpOnly)
85
+ end
84
86
 
85
- it 'should respect cookie options set in the configuration' do
86
- Timecop.freeze
87
+ it 'should respect cookie attributes set in the configuration' do
88
+ Timecop.freeze
87
89
 
88
- SafeCookies.configure do |config|
89
- config.register_cookie('foo', :expire_after => 3600, :path => '/special/path')
90
- end
90
+ SafeCookies.configure do |config|
91
+ config.register_cookie('foo', :expire_after => 3600, :path => '/special/path')
92
+ end
91
93
 
92
- stub_app_call(app)
93
- set_request_cookies(env, 'foo=bar')
94
- env['PATH_INFO'] = '/special/path/subfolder'
94
+ stub_app_call(app)
95
+ set_request_cookies(env, 'foo=bar')
96
+ env['PATH_INFO'] = '/special/path/subfolder'
95
97
 
96
- code, headers, response = subject.call(env)
97
- expected_expiry = Rack::Utils.rfc2822((Time.now + 3600).gmtime) # a special date format needed here
98
- headers['Set-Cookie'].should =~ %r(foo=bar; path=/special/path; expires=#{expected_expiry}; secure; HttpOnly)
98
+ code, headers, response = subject.call(env)
99
+ expected_expiry = (Time.now + 3600).gmtime.rfc2822 # a special date format needed here
100
+ headers['Set-Cookie'].should =~ %r(foo=bar; path=/special/path; expires=#{expected_expiry}; secure; HttpOnly)
101
+ end
102
+
99
103
  end
100
104
 
101
105
  context 'cookies set by the application' do
@@ -173,49 +177,44 @@ describe SafeCookies::Middleware do
173
177
  headers['Set-Cookie'].should =~ /js-data=json;.* secure/
174
178
  headers['Set-Cookie'].should_not =~ /js-data=json;.* HttpOnly/
175
179
  end
176
-
180
+
177
181
  end
178
182
 
179
- context 'unknown request cookies' do
183
+ context 'ignored cookies' do
180
184
 
181
- it 'should raise an error if there is an unknown cookie' do
182
- set_request_cookies(env, 'foo=bar')
183
-
184
- expect{ subject.call(env) }.to raise_error(SafeCookies::UnknownCookieError)
185
+ before do
186
+ stub_app_call(app)
187
+ set_request_cookies(env, '__utma=123', '__utmz=456')
185
188
  end
186
-
187
- it 'should not raise an error if the (unregistered) cookie was initially set by the application' do
188
- # application sets cookie
189
- stub_app_call(app, :application_cookies => 'foo=bar; path=/some/path; secure')
190
-
191
- code, headers, response = subject.call(env)
192
189
 
193
- received_cookies = extract_cookies(headers['Set-Cookie'])
194
- received_cookies.should include('foo=bar') # sanity check
195
-
196
- # client returns with the cookie, `app` and `subject` are different
197
- # objects than in the previous request
198
- other_app = stub('application')
199
- other_subject = described_class.new(other_app)
200
-
201
- stub_app_call(other_app)
202
- set_request_cookies(env, *received_cookies)
190
+ it 'does not rewrite ignored cookies given as string' do
191
+ SafeCookies.configure do |config|
192
+ config.ignore_cookie '__utma'
193
+ config.ignore_cookie '__utmz'
194
+ end
203
195
 
204
- other_subject.call(env)
196
+ code, headers, response = subject.call(env)
197
+ headers['Set-Cookie'].should_not =~ /__utm/
205
198
  end
206
-
207
- it 'should not raise an error if the cookie is listed in the cookie configuration' do
199
+
200
+ it 'does not rewrite ignored cookies given as regex' do
208
201
  SafeCookies.configure do |config|
209
- config.register_cookie('foo', :expire_after => 3600)
202
+ config.ignore_cookie /^__utm/
210
203
  end
211
-
212
- stub_app_call(app)
213
- set_request_cookies(env, 'foo=bar')
214
204
 
215
- subject.call(env)
205
+ code, headers, response = subject.call(env)
206
+ headers['Set-Cookie'].should_not =~ /__utm/
216
207
  end
208
+
209
+ end
217
210
 
218
- it 'allows overwriting the error mechanism' do
211
+
212
+ # The unknown cookies mechanism was more important when we were sending
213
+ # notifications on encountering unknown cookies. Perhaps, it will regain
214
+ # importance in the future.
215
+ context 'when a request has unknown cookies,' do
216
+
217
+ it 'allows overwriting the handling mechanism' do
219
218
  stub_app_call(app)
220
219
  set_request_cookies(env, 'foo=bar')
221
220
 
@@ -227,6 +226,69 @@ describe SafeCookies::Middleware do
227
226
  subject.instance_variable_get('@custom_method_called').should == true
228
227
  end
229
228
 
229
+ context 'and it is configured to log unknown cookies' do
230
+
231
+ before do
232
+ SafeCookies.configure do |config|
233
+ config.log_unknown_cookies = true
234
+ end
235
+ end
236
+
237
+ it 'logs an error message' do
238
+ stub_app_call(app)
239
+ set_request_cookies(env, 'foo=bar')
240
+
241
+ subject.should_receive(:log).with(/unknown cookies: foo/)
242
+ subject.call(env)
243
+ end
244
+
245
+ it 'does not log an error if the (unregistered) cookie was initially set by the application' do
246
+ # application sets cookie
247
+ stub_app_call(app, :application_cookies => 'foo=bar; path=/some/path; secure')
248
+
249
+ code, headers, response = subject.call(env)
250
+
251
+ received_cookies = extract_cookies(headers['Set-Cookie'])
252
+ received_cookies.should include('foo=bar') # sanity check
253
+
254
+ # client returns with the cookie, `app` and `subject` are different
255
+ # objects than in the previous request
256
+ other_app = stub('application')
257
+ other_subject = described_class.new(other_app)
258
+
259
+ stub_app_call(other_app)
260
+ set_request_cookies(env, *received_cookies)
261
+
262
+ other_subject.should_not_receive(:log)
263
+ other_subject.call(env)
264
+ end
265
+
266
+ it 'does not log an error if the cookie is listed in the cookie configuration' do
267
+ SafeCookies.configure do |config|
268
+ config.register_cookie('foo', :expire_after => 3600)
269
+ end
270
+
271
+ stub_app_call(app)
272
+ set_request_cookies(env, 'foo=bar')
273
+
274
+ subject.should_not_receive(:log)
275
+ subject.call(env)
276
+ end
277
+
278
+ it 'does not log an error if the cookie is ignored' do
279
+ SafeCookies.configure do |config|
280
+ config.ignore_cookie '__utma'
281
+ end
282
+
283
+ stub_app_call(app)
284
+ set_request_cookies(env, '__utma=tracking')
285
+
286
+ subject.should_not_receive(:log)
287
+ subject.call(env)
288
+ end
289
+
290
+ end
291
+
230
292
  end
231
293
 
232
294
  end
data/spec/util_spec.rb ADDED
@@ -0,0 +1,27 @@
1
+ require 'spec_helper'
2
+
3
+ describe SafeCookies::Util do
4
+
5
+ describe '.except!' do
6
+
7
+ before do
8
+ @hash = { 'a' => 1, 'ab' => 2, 'b' => 3 }
9
+ end
10
+
11
+ it 'deletes the given keys from the original hash' do
12
+ SafeCookies::Util.except!(@hash, 'a')
13
+ @hash.should == { 'ab' => 2, 'b' => 3 }
14
+ end
15
+
16
+ it 'deletes all keys that match the regex' do
17
+ SafeCookies::Util.except!(@hash, /b/)
18
+ @hash.should == { 'a' => 1 }
19
+ end
20
+
21
+ it 'returns the original hash' do
22
+ SafeCookies::Util.except!(@hash, /(?!)/).should == @hash
23
+ end
24
+
25
+ end
26
+
27
+ end
metadata CHANGED
@@ -1,69 +1,69 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: safe_cookies
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.5
4
+ version: 0.2.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Dominik Schöler
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2013-10-18 00:00:00.000000000 Z
11
+ date: 2021-04-06 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rack
15
15
  requirement: !ruby/object:Gem::Requirement
16
16
  requirements:
17
- - - ! '>='
17
+ - - ">="
18
18
  - !ruby/object:Gem::Version
19
19
  version: '0'
20
20
  type: :runtime
21
21
  prerelease: false
22
22
  version_requirements: !ruby/object:Gem::Requirement
23
23
  requirements:
24
- - - ! '>='
24
+ - - ">="
25
25
  - !ruby/object:Gem::Version
26
26
  version: '0'
27
27
  - !ruby/object:Gem::Dependency
28
28
  name: rspec
29
29
  requirement: !ruby/object:Gem::Requirement
30
30
  requirements:
31
- - - ! '>='
31
+ - - ">="
32
32
  - !ruby/object:Gem::Version
33
33
  version: '0'
34
34
  type: :development
35
35
  prerelease: false
36
36
  version_requirements: !ruby/object:Gem::Requirement
37
37
  requirements:
38
- - - ! '>='
38
+ - - ">="
39
39
  - !ruby/object:Gem::Version
40
40
  version: '0'
41
41
  - !ruby/object:Gem::Dependency
42
42
  name: timecop
43
43
  requirement: !ruby/object:Gem::Requirement
44
44
  requirements:
45
- - - ! '>='
45
+ - - ">="
46
46
  - !ruby/object:Gem::Version
47
47
  version: '0'
48
48
  type: :development
49
49
  prerelease: false
50
50
  version_requirements: !ruby/object:Gem::Requirement
51
51
  requirements:
52
- - - ! '>='
52
+ - - ">="
53
53
  - !ruby/object:Gem::Version
54
54
  version: '0'
55
55
  - !ruby/object:Gem::Dependency
56
56
  name: debugger
57
57
  requirement: !ruby/object:Gem::Requirement
58
58
  requirements:
59
- - - ! '>='
59
+ - - ">="
60
60
  - !ruby/object:Gem::Version
61
61
  version: '0'
62
62
  type: :development
63
63
  prerelease: false
64
64
  version_requirements: !ruby/object:Gem::Requirement
65
65
  requirements:
66
- - - ! '>='
66
+ - - ">="
67
67
  - !ruby/object:Gem::Version
68
68
  version: '0'
69
69
  description: Make all cookies `secure` and `HttpOnly`.
@@ -73,7 +73,7 @@ executables: []
73
73
  extensions: []
74
74
  extra_rdoc_files: []
75
75
  files:
76
- - .gitignore
76
+ - ".gitignore"
77
77
  - Gemfile
78
78
  - LICENSE
79
79
  - README.md
@@ -89,8 +89,10 @@ files:
89
89
  - spec/cookie_path_fix_spec.rb
90
90
  - spec/safe_cookies_spec.rb
91
91
  - spec/spec_helper.rb
92
+ - spec/util_spec.rb
92
93
  homepage: http://www.makandra.de
93
- licenses: []
94
+ licenses:
95
+ - MIT
94
96
  metadata: {}
95
97
  post_install_message:
96
98
  rdoc_options: []
@@ -98,17 +100,16 @@ require_paths:
98
100
  - lib
99
101
  required_ruby_version: !ruby/object:Gem::Requirement
100
102
  requirements:
101
- - - ! '>='
103
+ - - ">="
102
104
  - !ruby/object:Gem::Version
103
105
  version: '0'
104
106
  required_rubygems_version: !ruby/object:Gem::Requirement
105
107
  requirements:
106
- - - ! '>='
108
+ - - ">="
107
109
  - !ruby/object:Gem::Version
108
110
  version: '0'
109
111
  requirements: []
110
- rubyforge_project:
111
- rubygems_version: 2.1.2
112
+ rubygems_version: 3.2.15
112
113
  signing_key:
113
114
  specification_version: 4
114
115
  summary: Make all cookies `secure` and `HttpOnly`.
@@ -117,3 +118,4 @@ test_files:
117
118
  - spec/cookie_path_fix_spec.rb
118
119
  - spec/safe_cookies_spec.rb
119
120
  - spec/spec_helper.rb
121
+ - spec/util_spec.rb