safe_cookies 0.1.3 → 0.1.4
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.
- checksums.yaml +15 -0
- data/README.md +63 -18
- data/lib/safe_cookies/configuration.rb +69 -0
- data/lib/safe_cookies/cookie_path_fix.rb +64 -0
- data/lib/safe_cookies/util.rb +17 -0
- data/lib/safe_cookies/version.rb +1 -1
- data/lib/safe_cookies.rb +119 -57
- data/safe_cookies.gemspec +2 -2
- data/spec/configuration_spec.rb +86 -0
- data/spec/cookie_path_fix_spec.rb +157 -0
- data/spec/safe_cookies_spec.rb +187 -94
- data/spec/spec_helper.rb +20 -0
- metadata +69 -84
checksums.yaml
ADDED
@@ -0,0 +1,15 @@
|
|
1
|
+
---
|
2
|
+
!binary "U0hBMQ==":
|
3
|
+
metadata.gz: !binary |-
|
4
|
+
MTlmZjRlYWYyZDJmZjZmOTZhNDcyNGFhZDMyODE0Nzk3YTRmN2I3ZA==
|
5
|
+
data.tar.gz: !binary |-
|
6
|
+
MGY4OGY1Mjk1OGNiNDViZDA3Yjk2NTdkM2UzMGI0ZWUyMTZkYzQ4MA==
|
7
|
+
SHA512:
|
8
|
+
metadata.gz: !binary |-
|
9
|
+
M2FmYjk3YjYxYTA1MWQ0MmIzYzMzMDcwNzE5NGMzYTc2MGM0ZmZiNTBjMmI5
|
10
|
+
MzU5MzEwNjE0OTY4M2Q2ZWEzYjQxZGRiYmVlMTE2YmY5YjZjYTYwM2EyMTcz
|
11
|
+
ZDBhNTRiMTE4N2ExYWIzNjhhMDIyOWQzMmZhOWRjYjkyNjQ5MjQ=
|
12
|
+
data.tar.gz: !binary |-
|
13
|
+
MGVkY2ZhM2UzNmYyMDhlYjBiZGE4Y2Q3MjM3MzU3MTFmZTJmMzg2YTJkYTY4
|
14
|
+
MjBkYWZjYzk1MjRkYzljYWMwNGRkYWE4OTIzMjRhYjc3ZWYxZDY4MjJmZmVm
|
15
|
+
NWQyYTk4ZTZhNjA1YWY0YjFiMmI3NWMxZjI2MTEyYmI3YzM3MjI=
|
data/README.md
CHANGED
@@ -1,10 +1,10 @@
|
|
1
1
|
# SafeCookies
|
2
2
|
|
3
|
-
This Gem brings a
|
3
|
+
This Gem brings a middleware that will make all cookies secure. In detail, it will:
|
4
4
|
|
5
|
-
* set all new cookies 'HttpOnly', unless specified otherwise
|
6
|
-
* set all new cookies 'secure', if the request came via HTTPS and not specified otherwise
|
7
|
-
* rewrite
|
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
8
|
|
9
9
|
## Installation
|
10
10
|
|
@@ -12,7 +12,7 @@ Add this line to your application's Gemfile:
|
|
12
12
|
|
13
13
|
gem 'safe_cookies'
|
14
14
|
|
15
|
-
|
15
|
+
Then run:
|
16
16
|
|
17
17
|
$ bundle
|
18
18
|
|
@@ -20,25 +20,70 @@ Or install it yourself as:
|
|
20
20
|
|
21
21
|
$ gem install safe_cookies
|
22
22
|
|
23
|
+
|
23
24
|
## Usage
|
24
25
|
|
25
|
-
|
26
|
+
### Step 1
|
27
|
+
**Rails 3**: add the following line in config/application.rb:
|
28
|
+
|
29
|
+
class Application < Rails::Application
|
30
|
+
# ...
|
31
|
+
config.middleware.insert_before ActionDispatch::Cookies, SafeCookies::Middleware
|
32
|
+
end
|
33
|
+
|
34
|
+
**Rails 2:** add the following lines in config/environment.rb:
|
26
35
|
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
36
|
+
Rails::Initializer.run do |config|
|
37
|
+
# ...
|
38
|
+
require 'safe_cookies'
|
39
|
+
config.middleware.insert_before ActionController::Session::CookieStore, SafeCookies::Middleware
|
40
|
+
end
|
41
|
+
|
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):
|
45
|
+
|
46
|
+
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
|
51
|
+
end
|
32
52
|
|
33
53
|
This will have the `default_language` cookie not made secure, the `javascript_data` cookie
|
34
|
-
not made
|
35
|
-
`last_action` cookie with an expiry of 30 days, making both of them secure and
|
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
|
+
|
58
|
+
### Step 3
|
59
|
+
Override `SafeCookies::Middleware#handle_unknown_cookies(cookies)` (see "Dealing with unregistered cookies" below).
|
60
|
+
|
61
|
+
|
62
|
+
## Dealing with unregistered cookies
|
63
|
+
|
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.
|
69
|
+
|
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).
|
75
|
+
|
76
|
+
You should not ignore an unregistered cookie, but instead register it.
|
36
77
|
|
37
|
-
## About Rails and Cookies
|
38
78
|
|
39
|
-
|
79
|
+
## Fix cookie paths
|
40
80
|
|
41
|
-
|
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.
|
42
84
|
|
43
|
-
|
44
|
-
|
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.
|
@@ -0,0 +1,69 @@
|
|
1
|
+
module SafeCookies
|
2
|
+
|
3
|
+
MissingOptionError = Class.new(StandardError)
|
4
|
+
|
5
|
+
class << self
|
6
|
+
attr_accessor :configuration
|
7
|
+
|
8
|
+
def configure
|
9
|
+
self.configuration ||= Configuration.new
|
10
|
+
yield(configuration)
|
11
|
+
end
|
12
|
+
|
13
|
+
end
|
14
|
+
|
15
|
+
class Configuration
|
16
|
+
attr_reader :registered_cookies, :fix_cookie_paths, :correct_cookie_paths_timestamp
|
17
|
+
|
18
|
+
def initialize
|
19
|
+
self.registered_cookies = {}
|
20
|
+
self.insecure_cookies = []
|
21
|
+
self.scriptable_cookies = []
|
22
|
+
end
|
23
|
+
|
24
|
+
# Register cookies you expect to receive. The middleware will rewrite all
|
25
|
+
# registered cookies it receives, making them both secure and http_only.
|
26
|
+
#
|
27
|
+
# Unfortunately, the client won't ever tell us if the cookie was originally
|
28
|
+
# sent with flags such as "secure" or which expiry date it currently has:
|
29
|
+
# http://tools.ietf.org/html/rfc6265#section-4.2.2
|
30
|
+
#
|
31
|
+
# Therefore, specify an expiry, and more options if needed:
|
32
|
+
#
|
33
|
+
# :expire_after => 1.year
|
34
|
+
# :secure => false
|
35
|
+
# :http_only = false
|
36
|
+
# :path => '/foo/path'
|
37
|
+
#
|
38
|
+
def register_cookie(name, options)
|
39
|
+
options.has_key?(:expire_after) or raise MissingOptionError.new("Cookie #{name.inspect} was registered without an expiry")
|
40
|
+
raise NotImplementedError if options.has_key?(:domain)
|
41
|
+
|
42
|
+
registered_cookies[name.to_s] = (options || {}).freeze
|
43
|
+
insecure_cookies << name if options[:secure] == false
|
44
|
+
scriptable_cookies << name if options[:http_only] == false
|
45
|
+
end
|
46
|
+
|
47
|
+
def fix_paths(options = {})
|
48
|
+
options.has_key?(:for_cookies_secured_before) or raise MissingOptionError.new("Was told to fix paths without the :for_cookies_secured_before timestamp.")
|
49
|
+
|
50
|
+
self.fix_cookie_paths = true
|
51
|
+
self.correct_cookie_paths_timestamp = options[:for_cookies_secured_before]
|
52
|
+
end
|
53
|
+
|
54
|
+
def insecure_cookie?(name)
|
55
|
+
insecure_cookies.include? name
|
56
|
+
end
|
57
|
+
|
58
|
+
def scriptable_cookie?(name)
|
59
|
+
scriptable_cookies.include? name
|
60
|
+
end
|
61
|
+
|
62
|
+
private
|
63
|
+
|
64
|
+
attr_accessor :insecure_cookies, :scriptable_cookies
|
65
|
+
attr_writer :registered_cookies, :fix_cookie_paths, :correct_cookie_paths_timestamp
|
66
|
+
|
67
|
+
end
|
68
|
+
|
69
|
+
end
|
@@ -0,0 +1,64 @@
|
|
1
|
+
module SafeCookies
|
2
|
+
module CookiePathFix
|
3
|
+
|
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.
|
7
|
+
#
|
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
|
+
# 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
|
18
|
+
delete_cookie_for_current_directory(SafeCookies::SECURED_COOKIE_NAME)
|
19
|
+
|
20
|
+
# Delete this cookie here, so the middleware will secure all cookies anew.
|
21
|
+
@request.cookies.delete(SafeCookies::SECURED_COOKIE_NAME)
|
22
|
+
end
|
23
|
+
|
24
|
+
private
|
25
|
+
|
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
|
32
|
+
end
|
33
|
+
|
34
|
+
# Delete cookies by giving them an expiry in the past,
|
35
|
+
# cf. https://tools.ietf.org/html/rfc6265#section-4.1.2.
|
36
|
+
#
|
37
|
+
# Most important, as specified in
|
38
|
+
# https://tools.ietf.org/html/rfc6265#section-4.1.2.4 and in section 5.1.4,
|
39
|
+
# cookies set without a path will be set for the current "directory", that is:
|
40
|
+
#
|
41
|
+
# > ... the characters of the uri-path from the first character up
|
42
|
+
# > to, but not including, the right-most %x2F ("/").
|
43
|
+
#
|
44
|
+
# However, Firefox includes the right-most slash when guessing the cookie path,
|
45
|
+
# so we must resort to letting browsers estimate the deletion cookie path again.
|
46
|
+
def delete_cookie_for_current_directory(cookie_name)
|
47
|
+
current_directory_is_not_root = @request.path[%r(^/[^/]+/[^\?]+), 0]
|
48
|
+
|
49
|
+
if current_directory_is_not_root
|
50
|
+
one_week = (7 * 24 * 60 * 60)
|
51
|
+
set_cookie!(cookie_name, "", :path => nil, :expire_after => -one_week)
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
def secured_old_cookies_timestamp
|
56
|
+
Time.rfc2822(@request.cookies[SafeCookies::SECURED_COOKIE_NAME])
|
57
|
+
rescue ArgumentError
|
58
|
+
# If we cannot parse the secured_old_cookies time,
|
59
|
+
# assume it was before we noticed the bug to ensure
|
60
|
+
# broken cookie paths will be fixed.
|
61
|
+
Time.parse "2013-08-25 0:00"
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
@@ -0,0 +1,17 @@
|
|
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
|
+
}
|
9
|
+
|
10
|
+
# Normalize the result of Hash#select
|
11
|
+
# (Ruby 1.8 returns an Array, Ruby 1.9 returns a Hash)
|
12
|
+
Hash[sliced_hash]
|
13
|
+
end
|
14
|
+
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
data/lib/safe_cookies/version.rb
CHANGED
data/lib/safe_cookies.rb
CHANGED
@@ -1,49 +1,55 @@
|
|
1
1
|
# -*- encoding: utf-8 -*-
|
2
|
+
require "safe_cookies/configuration"
|
3
|
+
require "safe_cookies/cookie_path_fix"
|
4
|
+
require "safe_cookies/util"
|
2
5
|
require "safe_cookies/version"
|
3
6
|
require "rack"
|
4
7
|
|
5
8
|
module SafeCookies
|
6
|
-
class Middleware
|
7
9
|
|
10
|
+
UnknownCookieError = Class.new(StandardError)
|
11
|
+
|
12
|
+
CACHE_COOKIE_NAME = '_safe_cookies__known_cookies'
|
13
|
+
SECURED_COOKIE_NAME = 'secured_old_cookies'
|
14
|
+
HELPER_COOKIES_LIFETIME = 10 * 365 * 24 * 60 * 60 # 10 years
|
8
15
|
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
# sent with flags such as "secure" or which expiry date it currently has:
|
15
|
-
# http://tools.ietf.org/html/rfc6265#section-4.2.2
|
16
|
-
#
|
17
|
-
# The :non_secure option specifies cookies that will not be made secure. Use
|
18
|
-
# this for storing site usage settings like filters etc. that need to be available
|
19
|
-
# when not on HTTPS (this should rarely be the case).
|
20
|
-
#
|
21
|
-
# The :non_http_only option is analog, use it for storing data you want to access
|
22
|
-
# with javascript.
|
16
|
+
class Middleware
|
17
|
+
|
18
|
+
include CookiePathFix
|
19
|
+
|
20
|
+
KNOWN_COOKIES_DIVIDER = '|'
|
23
21
|
|
24
|
-
options = options.dup
|
25
22
|
|
23
|
+
def initialize(app)
|
26
24
|
@app = app
|
27
|
-
@
|
28
|
-
@non_http_only = (options.delete(:non_http_only) || []).map(&:to_s)
|
29
|
-
@cookies_to_update = options
|
25
|
+
@configuration = SafeCookies.configuration or raise "Don't know what to do without configuration"
|
30
26
|
end
|
31
27
|
|
32
28
|
def call(env)
|
33
|
-
|
34
|
-
|
29
|
+
reset_instance_variables
|
30
|
+
|
31
|
+
@request = Rack::Request.new(env)
|
32
|
+
ensure_no_unknown_cookies!
|
33
|
+
|
34
|
+
status, @headers, body = @app.call(env)
|
35
35
|
|
36
|
-
|
37
|
-
|
36
|
+
fix_cookie_paths if fix_cookie_paths?
|
37
|
+
rewrite_request_cookies unless cookies_have_been_rewritten_before
|
38
|
+
cache_application_cookies
|
39
|
+
rewrite_application_cookies
|
38
40
|
|
39
|
-
[ status, headers, body ]
|
41
|
+
[ status, @headers, body ]
|
40
42
|
end
|
41
43
|
|
42
44
|
private
|
45
|
+
|
46
|
+
def reset_instance_variables
|
47
|
+
@request, @headers = nil
|
48
|
+
end
|
43
49
|
|
44
50
|
def secure(cookie)
|
45
51
|
# Regexp from https://github.com/tobmatth/rack-ssl-enforcer/
|
46
|
-
if
|
52
|
+
if should_be_secure?(cookie) and cookie !~ /(^|;\s)secure($|;)/
|
47
53
|
"#{cookie}; secure"
|
48
54
|
else
|
49
55
|
cookie
|
@@ -51,40 +57,46 @@ module SafeCookies
|
|
51
57
|
end
|
52
58
|
|
53
59
|
def http_only(cookie)
|
54
|
-
if
|
60
|
+
if should_be_http_only?(cookie) and cookie !~ /(^|;\s)HttpOnly($|;)/
|
55
61
|
"#{cookie}; HttpOnly"
|
56
62
|
else
|
57
63
|
cookie
|
58
64
|
end
|
59
65
|
end
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
66
|
+
|
67
|
+
# This method takes all cookies sent with the request and rewrites them,
|
68
|
+
# making them both secure and http-only (unless specified otherwise in
|
69
|
+
# the configuration).
|
70
|
+
# With the SECURED_COOKIE_NAME cookie we remember the exact time that we
|
71
|
+
# rewrote the cookies.
|
72
|
+
def rewrite_request_cookies
|
73
|
+
if @request.cookies.any?
|
74
|
+
registered_cookies_in_request.each do |registered_cookie, options|
|
75
|
+
value = @request.cookies[registered_cookie]
|
76
|
+
|
77
|
+
set_cookie!(registered_cookie, value, options)
|
70
78
|
end
|
79
|
+
|
80
|
+
formatted_now = Rack::Utils.rfc2822(Time.now.gmtime)
|
81
|
+
set_cookie!(SECURED_COOKIE_NAME, formatted_now, :expire_after => HELPER_COOKIES_LIFETIME)
|
71
82
|
end
|
72
|
-
set_secure_cookie!(headers, 'secured_old_cookies', Rack::Utils.rfc2822(Time.now.gmtime))
|
73
83
|
end
|
74
84
|
|
75
|
-
def
|
76
|
-
options =
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
85
|
+
def set_cookie!(name, value, options)
|
86
|
+
options = options.dup
|
87
|
+
expire_after = options.delete(:expire_after)
|
88
|
+
|
89
|
+
options[:expires] = Time.now + expire_after if expire_after
|
90
|
+
options[:path] = '/' unless options.has_key?(:path) # allow setting path = nil
|
91
|
+
options[:value] = value
|
92
|
+
options[:secure] = should_be_secure?(name)
|
93
|
+
options[:httponly] = should_be_http_only?(name)
|
94
|
+
|
95
|
+
Rack::Utils.set_cookie_header!(@headers, name, options)
|
84
96
|
end
|
85
97
|
|
86
|
-
def
|
87
|
-
cookies = headers['Set-Cookie']
|
98
|
+
def rewrite_application_cookies
|
99
|
+
cookies = @headers['Set-Cookie']
|
88
100
|
if cookies
|
89
101
|
# Rails 2.3 / Rack 1.1 offers an array which is actually nice.
|
90
102
|
cookies = cookies.split("\n") unless cookies.is_a?(Array)
|
@@ -103,22 +115,72 @@ module SafeCookies
|
|
103
115
|
# It contains more information than the "HTTP_COOKIE" header from the
|
104
116
|
# browser's request contained, so a `Rack::Request` can't parse it for
|
105
117
|
# us. A `Rack::Response` doesn't offer a way either.
|
106
|
-
headers['Set-Cookie'] = cookies.join("\n")
|
118
|
+
@headers['Set-Cookie'] = cookies.join("\n")
|
119
|
+
end
|
120
|
+
end
|
121
|
+
|
122
|
+
def should_be_secure?(cookie)
|
123
|
+
cookie_name = cookie.split('=').first.strip
|
124
|
+
ssl? and not @configuration.insecure_cookie?(cookie_name)
|
125
|
+
end
|
126
|
+
|
127
|
+
def ssl?
|
128
|
+
if @request.respond_to?(:ssl?)
|
129
|
+
@request.ssl?
|
130
|
+
else
|
131
|
+
# older Rack versions
|
132
|
+
@request.scheme == 'https'
|
133
|
+
end
|
134
|
+
end
|
135
|
+
|
136
|
+
def should_be_http_only?(cookie)
|
137
|
+
cookie_name = cookie.split('=').first.strip
|
138
|
+
not @configuration.scriptable_cookie?(cookie_name)
|
139
|
+
end
|
140
|
+
|
141
|
+
def ensure_no_unknown_cookies!
|
142
|
+
request_cookies = @request.cookies.keys.map(&:to_s)
|
143
|
+
unknown_cookies = request_cookies - known_cookies
|
144
|
+
|
145
|
+
if unknown_cookies.any?
|
146
|
+
handle_unknown_cookies(unknown_cookies)
|
147
|
+
end
|
148
|
+
end
|
149
|
+
|
150
|
+
def handle_unknown_cookies(cookies)
|
151
|
+
raise SafeCookies::UnknownCookieError.new("Request for '#{@request.url}' had unknown cookies: #{cookies.join(', ')}")
|
152
|
+
end
|
153
|
+
|
154
|
+
def cache_application_cookies
|
155
|
+
new_application_cookies = @headers['Set-Cookie']
|
156
|
+
|
157
|
+
if new_application_cookies
|
158
|
+
new_application_cookies = new_application_cookies.join("\n") if new_application_cookies.is_a?(Array)
|
159
|
+
application_cookies = cached_application_cookies + new_application_cookies.scan(/(?=^|\n)[^\n;,=]+/i)
|
160
|
+
application_cookies_string = application_cookies.uniq.join(KNOWN_COOKIES_DIVIDER)
|
161
|
+
|
162
|
+
set_cookie!(CACHE_COOKIE_NAME, application_cookies_string, :expire_after => HELPER_COOKIES_LIFETIME)
|
107
163
|
end
|
108
164
|
end
|
109
165
|
|
110
|
-
def
|
111
|
-
|
112
|
-
|
166
|
+
def cached_application_cookies
|
167
|
+
cache_cookie = @request.cookies[CACHE_COOKIE_NAME] || ""
|
168
|
+
cache_cookie.split(KNOWN_COOKIES_DIVIDER)
|
169
|
+
end
|
170
|
+
|
171
|
+
def known_cookies
|
172
|
+
known = [CACHE_COOKIE_NAME, SECURED_COOKIE_NAME]
|
173
|
+
known += cached_application_cookies
|
174
|
+
known += @configuration.registered_cookies.keys
|
113
175
|
end
|
114
176
|
|
115
|
-
def
|
116
|
-
|
117
|
-
not @non_http_only.include?(name)
|
177
|
+
def cookies_have_been_rewritten_before
|
178
|
+
@request.cookies.has_key? SECURED_COOKIE_NAME
|
118
179
|
end
|
119
180
|
|
120
|
-
|
121
|
-
|
181
|
+
# returns those of the registered cookies that appear in the request
|
182
|
+
def registered_cookies_in_request
|
183
|
+
Util.slice(@configuration.registered_cookies, *@request.cookies.keys)
|
122
184
|
end
|
123
185
|
|
124
186
|
end
|
data/safe_cookies.gemspec
CHANGED
@@ -4,8 +4,8 @@ require File.expand_path('../lib/safe_cookies/version', __FILE__)
|
|
4
4
|
Gem::Specification.new do |gem|
|
5
5
|
gem.authors = ["Dominik Schöler"]
|
6
6
|
gem.email = ["dominik.schoeler@makandra.de"]
|
7
|
-
gem.description = %q{Make cookies
|
8
|
-
gem.summary = %q{Make cookies
|
7
|
+
gem.description = %q{Make all cookies `secure` and `HttpOnly`.}
|
8
|
+
gem.summary = %q{Make all cookies `secure` and `HttpOnly`.}
|
9
9
|
gem.homepage = "http://www.makandra.de"
|
10
10
|
|
11
11
|
gem.files = `git ls-files`.split($\)
|
@@ -0,0 +1,86 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
require 'spec_helper'
|
3
|
+
|
4
|
+
describe SafeCookies::Middleware do
|
5
|
+
|
6
|
+
it 'does not allow registered cookies to be altered' do
|
7
|
+
SafeCookies.configure do |config|
|
8
|
+
config.register_cookie('filter', :expire_after => 3600)
|
9
|
+
end
|
10
|
+
|
11
|
+
filter_options = SafeCookies.configuration.registered_cookies['filter']
|
12
|
+
expect { filter_options[:foo] = 'bar' }.to raise_error(Exception, /can't modify frozen hash/i)
|
13
|
+
end
|
14
|
+
|
15
|
+
describe '.configure' do
|
16
|
+
|
17
|
+
it 'currently does not support the :domain cookie option' do
|
18
|
+
registration_with_domain = lambda do
|
19
|
+
SafeCookies.configure do |config|
|
20
|
+
config.register_cookie('filter', :domain => 'example.com', :expire_after => 3600)
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
expect(®istration_with_domain).to raise_error(NotImplementedError)
|
25
|
+
end
|
26
|
+
|
27
|
+
describe 'register_cookie' do
|
28
|
+
|
29
|
+
context 'cookie name formatting' do
|
30
|
+
|
31
|
+
let(:set_cookie) do
|
32
|
+
# These tests for the Configuration module require an integration with
|
33
|
+
# the middleware itself. Therefore, we need to actually use it.
|
34
|
+
|
35
|
+
app = stub('app')
|
36
|
+
env = { 'HTTPS' => 'on' }
|
37
|
+
stub_app_call(app, :application_cookies => 'cookie_name=value')
|
38
|
+
|
39
|
+
middleware = described_class.new(app)
|
40
|
+
code, headers, response = middleware.call(env)
|
41
|
+
headers['Set-Cookie']
|
42
|
+
end
|
43
|
+
|
44
|
+
it 'understands cookies registered as symbol' do
|
45
|
+
SafeCookies.configure do |config|
|
46
|
+
config.register_cookie(:cookie_name, :expire_after => nil)
|
47
|
+
end
|
48
|
+
|
49
|
+
set_cookie.should =~ /cookie_name=value;.* secure; HttpOnly/
|
50
|
+
end
|
51
|
+
|
52
|
+
it 'understands cookies registered as string' do
|
53
|
+
SafeCookies.configure do |config|
|
54
|
+
config.register_cookie('cookie_name', :expire_after => nil)
|
55
|
+
end
|
56
|
+
|
57
|
+
set_cookie.should =~ /cookie_name=value;.* secure; HttpOnly/
|
58
|
+
end
|
59
|
+
|
60
|
+
end
|
61
|
+
|
62
|
+
it 'raises an error if a cookie is registered without passing its expiry' do
|
63
|
+
registration_without_expiry = lambda do
|
64
|
+
SafeCookies.configure do |config|
|
65
|
+
config.register_cookie(:filter, :some => :option)
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
expect(®istration_without_expiry).to raise_error(SafeCookies::MissingOptionError)
|
70
|
+
end
|
71
|
+
|
72
|
+
it 'allows nil as expiry (means session cookie)' do
|
73
|
+
registration_with_nil_expiry = lambda do
|
74
|
+
SafeCookies.configure do |config|
|
75
|
+
config.register_cookie(:filter, :expire_after => nil)
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
expect(®istration_with_nil_expiry).to_not raise_error(SafeCookies::MissingOptionError)
|
80
|
+
end
|
81
|
+
|
82
|
+
end
|
83
|
+
|
84
|
+
end
|
85
|
+
|
86
|
+
end
|
@@ -0,0 +1,157 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
require 'cgi'
|
3
|
+
|
4
|
+
describe SafeCookies::Middleware do
|
5
|
+
|
6
|
+
describe 'cookie path fix' do
|
7
|
+
|
8
|
+
subject { described_class.new(app) }
|
9
|
+
let(:app) { stub 'application' }
|
10
|
+
let(:env) { { 'HTTPS' => 'on' } }
|
11
|
+
|
12
|
+
before do
|
13
|
+
@now = Time.parse('2050-01-01 00:00')
|
14
|
+
Timecop.travel(@now)
|
15
|
+
end
|
16
|
+
|
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)}")
|
20
|
+
end
|
21
|
+
|
22
|
+
|
23
|
+
context 'rewriting previously secured cookies' do
|
24
|
+
|
25
|
+
before do
|
26
|
+
SafeCookies.configure do |config|
|
27
|
+
config.register_cookie('cookie_to_update', :expire_after => 3600)
|
28
|
+
config.fix_paths :for_cookies_secured_before => Time.parse('2050-01-02 00:00')
|
29
|
+
end
|
30
|
+
|
31
|
+
stub_app_call(app)
|
32
|
+
set_default_request_cookies
|
33
|
+
end
|
34
|
+
|
35
|
+
it 'updates the timestamp on the root secured_old_cookies cookie' do
|
36
|
+
code, headers, response = subject.call(env)
|
37
|
+
|
38
|
+
updated_secured_old_cookies_timestamp = 'Fri%2C+31+Dec+2049+23%3A00%3A00+-0000'
|
39
|
+
headers['Set-Cookie'].should =~ /secured_old_cookies=#{Regexp.escape updated_secured_old_cookies_timestamp}; path=\/;/
|
40
|
+
end
|
41
|
+
|
42
|
+
it 'sets the cookie path to "/"' do
|
43
|
+
code, headers, response = subject.call(env)
|
44
|
+
headers['Set-Cookie'].should =~ /cookie_to_update=some_data;.*path=\/;/
|
45
|
+
end
|
46
|
+
|
47
|
+
it 'deletes cookies for the current "directory"' do
|
48
|
+
env['PATH_INFO'] = '/complex/sub/path'
|
49
|
+
|
50
|
+
code, headers, response = subject.call(env)
|
51
|
+
set_cookie = headers['Set-Cookie']
|
52
|
+
|
53
|
+
# overwrite the cookie with an empty value
|
54
|
+
set_cookie.should =~ /cookie_to_update=;/
|
55
|
+
|
56
|
+
# the deletion cookie must not have a path, so browsers use their own implementation of cookie path
|
57
|
+
# determination, the same they used when the cookie was implicitly set on the wrong path
|
58
|
+
set_cookie.should_not =~ %r(cookie_to_update=;.*path=)
|
59
|
+
|
60
|
+
# cookies are deleted by giving them an expiry in the past
|
61
|
+
deletion_expiry = set_cookie[/cookie_to_update=;.*expires=([^;]+)/, 1]
|
62
|
+
Time.parse(deletion_expiry).should < @now
|
63
|
+
end
|
64
|
+
|
65
|
+
it 'does not delete cookies from root ("/") requests, since root cookies are the default we expect' do
|
66
|
+
env['PATH_INFO'] = '/'
|
67
|
+
|
68
|
+
code, headers, response = subject.call(env)
|
69
|
+
headers['Set-Cookie'].should_not =~ /cookie_to_update=;/
|
70
|
+
end
|
71
|
+
|
72
|
+
it 'does not delete cookies from first-level paths like "/first_level", since their "directory" is "/"' do
|
73
|
+
env['PATH_INFO'] = '/first_level'
|
74
|
+
|
75
|
+
code, headers, response = subject.call(env)
|
76
|
+
headers['Set-Cookie'].should_not =~ /cookie_to_update=;/
|
77
|
+
end
|
78
|
+
|
79
|
+
it 'does not delete cookies from first-level paths like "/first_level/", since their "directory" is "/"' do
|
80
|
+
env['PATH_INFO'] = '/first_level'
|
81
|
+
|
82
|
+
code, headers, response = subject.call(env)
|
83
|
+
headers['Set-Cookie'].should_not =~ /cookie_to_update=;/
|
84
|
+
end
|
85
|
+
|
86
|
+
it 'should not be confused by query parameters' do
|
87
|
+
env['PATH_INFO'] = '/some/sub/directory/with?query=params&and=/another/path'
|
88
|
+
|
89
|
+
code, headers, response = subject.call(env)
|
90
|
+
headers['Set-Cookie'].should =~ %r(cookie_to_update=;)
|
91
|
+
end
|
92
|
+
|
93
|
+
it 'should not "fix" a path set by the application' do
|
94
|
+
stub_app_call(app, :application_cookies => 'new_cookie=NEW_DATA; path=/special/path')
|
95
|
+
env['PATH_INFO'] = '/special/path/sub/folder'
|
96
|
+
|
97
|
+
code, headers, response = subject.call(env)
|
98
|
+
headers['Set-Cookie'].should =~ %r(new_cookie=NEW_DATA;.*path=/special/path;)
|
99
|
+
end
|
100
|
+
|
101
|
+
it 'deletes the secured_old_cookies cookie on the current "directory", so future requests to that directory do not trigger a rewrite of all cookies' do
|
102
|
+
env['PATH_INFO'] = '/complex/sub/path'
|
103
|
+
|
104
|
+
code, headers, response = subject.call(env)
|
105
|
+
|
106
|
+
# delete the "directory" secured_old_cookies cookie ...
|
107
|
+
headers['Set-Cookie'].should =~ %r(secured_old_cookies=;)
|
108
|
+
# ... but do not delete the root secured_old_cookies cookie
|
109
|
+
headers['Set-Cookie'].should =~ %r(secured_old_cookies=\w+.*path=/;)
|
110
|
+
end
|
111
|
+
|
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=\/;/
|
117
|
+
end
|
118
|
+
|
119
|
+
end
|
120
|
+
|
121
|
+
it 'does not rewrite previously secured cookies if not told so' do
|
122
|
+
SafeCookies.configure do |config|
|
123
|
+
config.register_cookie('cookie_to_update', :expire_after => 3600)
|
124
|
+
# missing config.fix_paths
|
125
|
+
end
|
126
|
+
stub_app_call(app)
|
127
|
+
set_default_request_cookies
|
128
|
+
|
129
|
+
code, headers, response = subject.call(env)
|
130
|
+
headers['Set-Cookie'].should be_nil
|
131
|
+
end
|
132
|
+
|
133
|
+
it 'raises an error if told to fix cookie paths without specifying a date' do
|
134
|
+
fix_paths_without_timestamp = lambda do
|
135
|
+
SafeCookies.configure do |config|
|
136
|
+
config.fix_paths # missing :for_cookies_secured_before option
|
137
|
+
end
|
138
|
+
end
|
139
|
+
|
140
|
+
expect(&fix_paths_without_timestamp).to raise_error(SafeCookies::MissingOptionError)
|
141
|
+
end
|
142
|
+
|
143
|
+
it 'does not rewrite cookies that were secured after the correct_cookie_paths_timestamp' do
|
144
|
+
SafeCookies.configure do |config|
|
145
|
+
config.register_cookie('cookie_to_update', :expire_after => 3600)
|
146
|
+
config.fix_paths :for_cookies_secured_before => Time.parse('2050-01-02 00:00')
|
147
|
+
end
|
148
|
+
stub_app_call(app)
|
149
|
+
set_default_request_cookies(Time.parse('2050-01-03 00:00'))
|
150
|
+
|
151
|
+
code, headers, response = subject.call(env)
|
152
|
+
headers['Set-Cookie'].should be_nil
|
153
|
+
end
|
154
|
+
|
155
|
+
end
|
156
|
+
|
157
|
+
end
|
data/spec/safe_cookies_spec.rb
CHANGED
@@ -1,136 +1,229 @@
|
|
1
1
|
# -*- encoding: utf-8 -*-
|
2
2
|
require 'spec_helper'
|
3
3
|
|
4
|
-
# Explanation:
|
5
|
-
# app#call(env) is how the middleware calls the app
|
6
|
-
# returns the app's response
|
7
|
-
# subject#call(env) is how the middleware is called "from below"
|
8
|
-
# returns the response that is passed through the web server to the client
|
9
|
-
|
10
4
|
describe SafeCookies::Middleware do
|
11
5
|
|
6
|
+
subject { described_class.new(app) }
|
12
7
|
let(:app) { stub 'application' }
|
13
8
|
let(:env) { { 'HTTPS' => 'on' } }
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
9
|
+
|
10
|
+
it 'should rewrite registered request cookies as secure and http-only, but only once' do
|
11
|
+
SafeCookies.configure do |config|
|
12
|
+
config.register_cookie('foo', :expire_after => 3600)
|
13
|
+
end
|
14
|
+
|
15
|
+
# first request: rewrite cookie
|
16
|
+
stub_app_call(app)
|
17
|
+
set_request_cookies(env, 'foo=bar')
|
18
|
+
|
19
|
+
code, headers, response = subject.call(env)
|
20
|
+
headers['Set-Cookie'].should =~ /foo=bar;.* secure; HttpOnly/
|
22
21
|
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
env['HTTP_COOKIE'] = received_cookies.join(',')
|
22
|
+
# second request: do not rewrite cookie again
|
23
|
+
received_cookies = extract_cookies(headers['Set-Cookie'])
|
24
|
+
received_cookies.should include('foo=bar') # sanity check
|
25
|
+
|
26
|
+
# client returns with the cookies, `app` and `subject` are different
|
27
|
+
# objects than in the previous request
|
28
|
+
other_app = stub('application')
|
29
|
+
other_subject = described_class.new(other_app)
|
30
|
+
|
31
|
+
stub_app_call(other_app)
|
32
|
+
set_request_cookies(env, *received_cookies)
|
35
33
|
|
36
|
-
|
37
|
-
|
38
|
-
end
|
34
|
+
code, headers, response = other_subject.call(env)
|
35
|
+
headers['Set-Cookie'].to_s.should == ''
|
39
36
|
end
|
40
|
-
|
41
|
-
it
|
42
|
-
app
|
37
|
+
|
38
|
+
it 'should not make cookies secure if the request was not secure' do
|
39
|
+
stub_app_call(app, :application_cookies => 'filter-settings=sort_by_date')
|
40
|
+
env['HTTPS'] = 'off'
|
43
41
|
|
44
42
|
code, headers, response = subject.call(env)
|
45
|
-
headers['Set-Cookie'].should
|
43
|
+
headers['Set-Cookie'].should include("filter-settings=sort_by_date")
|
44
|
+
headers['Set-Cookie'].should_not match(/\bsecure\b/i)
|
46
45
|
end
|
47
|
-
|
48
|
-
it
|
49
|
-
|
46
|
+
|
47
|
+
it 'expires the secured_old_cookies helper cookie in ten years' do
|
48
|
+
Timecop.freeze(Time.parse('2013-09-17 17:53'))
|
49
|
+
|
50
|
+
SafeCookies.configure do |config|
|
51
|
+
config.register_cookie('cookie_to_update', :expire_after => 3600)
|
52
|
+
end
|
50
53
|
|
54
|
+
set_request_cookies(env, 'cookie_to_update=some_data')
|
55
|
+
stub_app_call(app)
|
56
|
+
|
51
57
|
code, headers, response = subject.call(env)
|
52
|
-
|
58
|
+
|
59
|
+
headers['Set-Cookie'].should =~ /secured_old_cookies.*expires=Fri, 15 Sep 2023 \d\d:\d\d:\d\d/
|
53
60
|
end
|
54
61
|
|
55
|
-
it
|
56
|
-
|
57
|
-
|
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
|
58
66
|
|
67
|
+
set_request_cookies(env, 'my_old_cookie=foobar')
|
68
|
+
stub_app_call(app)
|
69
|
+
|
59
70
|
code, headers, response = subject.call(env)
|
60
|
-
|
61
|
-
headers['Set-Cookie'].
|
71
|
+
|
72
|
+
cookies = headers['Set-Cookie'].split("\n")
|
73
|
+
cookies.size.should == 3 # my_old_cookie and secured_old_cookies and _known_cookies
|
74
|
+
cookies.each do |cookie|
|
75
|
+
cookie.should include('; path=/;')
|
76
|
+
end
|
62
77
|
end
|
63
78
|
|
64
|
-
it
|
65
|
-
|
66
|
-
|
67
|
-
|
79
|
+
it 'should not alter cookie options coming from the application' do
|
80
|
+
stub_app_call(app, :application_cookies => 'cookie=data; path=/; expires=next_week')
|
81
|
+
|
68
82
|
code, headers, response = subject.call(env)
|
69
|
-
headers['Set-Cookie'].should
|
70
|
-
headers['Set-Cookie'].should_not match(/HttpOnly/i)
|
83
|
+
headers['Set-Cookie'].should =~ %r(cookie=data; path=/; expires=next_week; secure; HttpOnly)
|
71
84
|
end
|
72
85
|
|
73
|
-
it
|
74
|
-
|
75
|
-
env['HTTP_COOKIE'] = 'cookie=wert'
|
86
|
+
it 'should respect cookie options set in the configuration' do
|
87
|
+
Timecop.freeze
|
76
88
|
|
89
|
+
SafeCookies.configure do |config|
|
90
|
+
config.register_cookie('foo', :expire_after => 3600, :path => '/special/path')
|
91
|
+
end
|
92
|
+
|
93
|
+
stub_app_call(app)
|
94
|
+
set_request_cookies(env, 'foo=bar')
|
95
|
+
env['PATH_INFO'] = '/special/path/subfolder'
|
96
|
+
|
77
97
|
code, headers, response = subject.call(env)
|
78
|
-
|
98
|
+
expected_expiry = Rack::Utils.rfc2822((Time.now + 3600).gmtime) # a special date format needed here
|
99
|
+
headers['Set-Cookie'].should =~ %r(foo=bar; path=/special/path; expires=#{expected_expiry}; secure; HttpOnly)
|
79
100
|
end
|
101
|
+
|
102
|
+
context 'cookies set by the application' do
|
103
|
+
|
104
|
+
it 'should make application cookies secure and http-only' do
|
105
|
+
stub_app_call(app, :application_cookies => 'application_cookie=value')
|
80
106
|
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
107
|
+
code, headers, response = subject.call(env)
|
108
|
+
headers['Set-Cookie'].should =~ /application_cookie=value;.* secure; HttpOnly/
|
109
|
+
end
|
110
|
+
|
111
|
+
it 'should not make application cookies secure that are specified as non-secure' do
|
112
|
+
SafeCookies.configure do |config|
|
113
|
+
config.register_cookie('filter-settings', :expire_after => 3600, :secure => false)
|
114
|
+
end
|
85
115
|
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
116
|
+
stub_app_call(app, :application_cookies => 'filter-settings=sort_by_date')
|
117
|
+
|
118
|
+
code, headers, response = subject.call(env)
|
119
|
+
headers['Set-Cookie'].should include("filter-settings=sort_by_date")
|
120
|
+
headers['Set-Cookie'].should_not =~ /filter-settings=.*secure/i
|
121
|
+
end
|
91
122
|
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
123
|
+
it 'should not make application cookies http-only that are specified as non-http-only' do
|
124
|
+
SafeCookies.configure do |config|
|
125
|
+
config.register_cookie('javascript-cookie', :expire_after => 3600, :http_only => false)
|
126
|
+
end
|
96
127
|
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
128
|
+
stub_app_call(app, :application_cookies => 'javascript-cookie=xss')
|
129
|
+
|
130
|
+
code, headers, response = subject.call(env)
|
131
|
+
headers['Set-Cookie'].should include("javascript-cookie=xss")
|
132
|
+
headers['Set-Cookie'].should_not =~ /javascript-cookie=.*HttpOnly/i
|
133
|
+
end
|
102
134
|
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
env['HTTPS'] = 'off'
|
135
|
+
it 'should prefer the application cookie over a client cookie' do
|
136
|
+
stub_app_call(app, :application_cookies => 'cookie=from_application')
|
137
|
+
set_request_cookies(env, 'cookie=from_client,_safe_cookies__known_cookies=cookie')
|
107
138
|
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
it 'does not mutate an options hash passed to it' do
|
114
|
-
options = { :cookie1 => 3600, :non_secure => [:cookie2], :non_http_only => [:cookie3] }
|
115
|
-
described_class.new(app, options)
|
139
|
+
code, headers, response = subject.call(env)
|
140
|
+
headers['Set-Cookie'].should include("cookie=from_application")
|
141
|
+
headers['Set-Cookie'].should_not include("cookie=from_client")
|
142
|
+
end
|
116
143
|
|
117
|
-
|
118
|
-
|
119
|
-
|
144
|
+
end
|
145
|
+
|
146
|
+
context 'cookies sent by the client' do
|
147
|
+
|
148
|
+
it 'should not make request cookies secure that are specified as non-secure' do
|
149
|
+
SafeCookies.configure do |config|
|
150
|
+
config.register_cookie('filter', :expire_after => 3600, :secure => false)
|
151
|
+
end
|
152
|
+
|
153
|
+
stub_app_call(app)
|
154
|
+
set_request_cookies(env, 'filter=cars_only')
|
155
|
+
|
156
|
+
code, headers, response = subject.call(env)
|
157
|
+
headers['Set-Cookie'].should =~ /filter=cars_only;.* HttpOnly/
|
158
|
+
headers['Set-Cookie'].should_not =~ /filter=cars_only;.* secure/
|
159
|
+
end
|
160
|
+
|
161
|
+
it 'should not make request cookies http-only that are specified as non-http-only' do
|
162
|
+
SafeCookies.configure do |config|
|
163
|
+
config.register_cookie('js-data', :expire_after => 3600, :http_only => false)
|
164
|
+
end
|
165
|
+
|
166
|
+
stub_app_call(app)
|
167
|
+
set_request_cookies(env, 'js-data=json')
|
168
|
+
|
169
|
+
code, headers, response = subject.call(env)
|
170
|
+
headers['Set-Cookie'].should =~ /js-data=json;.* secure/
|
171
|
+
headers['Set-Cookie'].should_not =~ /js-data=json;.* HttpOnly/
|
172
|
+
end
|
173
|
+
|
120
174
|
end
|
121
175
|
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
|
176
|
+
context 'unknown request cookies' do
|
177
|
+
|
178
|
+
it 'should raise an error if there is an unknown cookie' do
|
179
|
+
set_request_cookies(env, 'foo=bar')
|
180
|
+
|
181
|
+
expect{ subject.call(env) }.to raise_error(SafeCookies::UnknownCookieError)
|
182
|
+
end
|
183
|
+
|
184
|
+
it 'should not raise an error if the (unregistered) cookie was initially set by the application' do
|
185
|
+
# application sets cookie
|
186
|
+
stub_app_call(app, :application_cookies => 'foo=bar; path=/some/path; secure')
|
187
|
+
|
188
|
+
code, headers, response = subject.call(env)
|
126
189
|
|
127
|
-
|
190
|
+
received_cookies = extract_cookies(headers['Set-Cookie'])
|
191
|
+
received_cookies.should include('foo=bar') # sanity check
|
192
|
+
|
193
|
+
# client returns with the cookie, `app` and `subject` are different
|
194
|
+
# objects than in the previous request
|
195
|
+
other_app = stub('application')
|
196
|
+
other_subject = described_class.new(other_app)
|
197
|
+
|
198
|
+
stub_app_call(other_app)
|
199
|
+
set_request_cookies(env, *received_cookies)
|
128
200
|
|
129
|
-
|
130
|
-
|
131
|
-
|
132
|
-
|
201
|
+
other_subject.call(env)
|
202
|
+
end
|
203
|
+
|
204
|
+
it 'should not raise an error if the cookie is listed in the cookie configuration' do
|
205
|
+
SafeCookies.configure do |config|
|
206
|
+
config.register_cookie('foo', :expire_after => 3600)
|
207
|
+
end
|
208
|
+
|
209
|
+
stub_app_call(app)
|
210
|
+
set_request_cookies(env, 'foo=bar')
|
211
|
+
|
212
|
+
subject.call(env)
|
133
213
|
end
|
214
|
+
|
215
|
+
it 'allows overwriting the error mechanism' do
|
216
|
+
stub_app_call(app)
|
217
|
+
set_request_cookies(env, 'foo=bar')
|
218
|
+
|
219
|
+
def subject.handle_unknown_cookies(*args)
|
220
|
+
@custom_method_called = true
|
221
|
+
end
|
222
|
+
|
223
|
+
subject.call(env)
|
224
|
+
subject.instance_variable_get('@custom_method_called').should == true
|
225
|
+
end
|
226
|
+
|
134
227
|
end
|
135
228
|
|
136
229
|
end
|
data/spec/spec_helper.rb
CHANGED
@@ -11,4 +11,24 @@ RSpec.configure do |config|
|
|
11
11
|
# the seed, which is printed after each run.
|
12
12
|
# --seed 1234
|
13
13
|
config.order = 'random'
|
14
|
+
|
15
|
+
config.before(:each) { SafeCookies.configure {} }
|
16
|
+
config.after(:each) {
|
17
|
+
SafeCookies.configuration = nil
|
18
|
+
Timecop.return
|
19
|
+
}
|
20
|
+
end
|
21
|
+
|
22
|
+
def stub_app_call(app, options = {})
|
23
|
+
env = {}
|
24
|
+
env['Set-Cookie'] = options[:application_cookies] if options[:application_cookies]
|
25
|
+
app.stub :call => [ stub, env, stub ]
|
26
|
+
end
|
27
|
+
|
28
|
+
def set_request_cookies(env, *cookies)
|
29
|
+
env['HTTP_COOKIE'] = cookies.join(',')
|
30
|
+
end
|
31
|
+
|
32
|
+
def extract_cookies(set_cookies_header)
|
33
|
+
set_cookies_header.scan(/(?=^|\n)[^\n;]+=[^\n;]+(?=;\s)/i)
|
14
34
|
end
|
metadata
CHANGED
@@ -1,119 +1,104 @@
|
|
1
|
-
--- !ruby/object:Gem::Specification
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
2
|
name: safe_cookies
|
3
|
-
version: !ruby/object:Gem::Version
|
4
|
-
|
5
|
-
prerelease:
|
6
|
-
segments:
|
7
|
-
- 0
|
8
|
-
- 1
|
9
|
-
- 3
|
10
|
-
version: 0.1.3
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.4
|
11
5
|
platform: ruby
|
12
|
-
authors:
|
13
|
-
-
|
6
|
+
authors:
|
7
|
+
- Dominik Schöler
|
14
8
|
autorequire:
|
15
9
|
bindir: bin
|
16
10
|
cert_chain: []
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
dependencies:
|
21
|
-
- !ruby/object:Gem::Dependency
|
11
|
+
date: 2013-09-25 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
22
14
|
name: rack
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
- !ruby/object:Gem::Version
|
29
|
-
hash: 3
|
30
|
-
segments:
|
31
|
-
- 0
|
32
|
-
version: "0"
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - ! '>='
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '0'
|
33
20
|
type: :runtime
|
34
|
-
version_requirements: *id001
|
35
|
-
- !ruby/object:Gem::Dependency
|
36
|
-
name: rspec
|
37
21
|
prerelease: false
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - ! '>='
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '0'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: rspec
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - ! '>='
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '0'
|
47
34
|
type: :development
|
48
|
-
version_requirements: *id002
|
49
|
-
- !ruby/object:Gem::Dependency
|
50
|
-
name: timecop
|
51
35
|
prerelease: false
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - ! '>='
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '0'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: timecop
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - ! '>='
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '0'
|
61
48
|
type: :development
|
62
|
-
|
63
|
-
|
64
|
-
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - ! '>='
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '0'
|
55
|
+
description: Make all cookies `secure` and `HttpOnly`.
|
56
|
+
email:
|
65
57
|
- dominik.schoeler@makandra.de
|
66
58
|
executables: []
|
67
|
-
|
68
59
|
extensions: []
|
69
|
-
|
70
60
|
extra_rdoc_files: []
|
71
|
-
|
72
|
-
files:
|
61
|
+
files:
|
73
62
|
- .gitignore
|
74
63
|
- Gemfile
|
75
64
|
- LICENSE
|
76
65
|
- README.md
|
77
66
|
- Rakefile
|
78
67
|
- lib/safe_cookies.rb
|
68
|
+
- lib/safe_cookies/configuration.rb
|
69
|
+
- lib/safe_cookies/cookie_path_fix.rb
|
70
|
+
- lib/safe_cookies/util.rb
|
79
71
|
- lib/safe_cookies/version.rb
|
80
72
|
- safe_cookies.gemspec
|
73
|
+
- spec/configuration_spec.rb
|
74
|
+
- spec/cookie_path_fix_spec.rb
|
81
75
|
- spec/safe_cookies_spec.rb
|
82
76
|
- spec/spec_helper.rb
|
83
|
-
has_rdoc: true
|
84
77
|
homepage: http://www.makandra.de
|
85
78
|
licenses: []
|
86
|
-
|
79
|
+
metadata: {}
|
87
80
|
post_install_message:
|
88
81
|
rdoc_options: []
|
89
|
-
|
90
|
-
require_paths:
|
82
|
+
require_paths:
|
91
83
|
- lib
|
92
|
-
required_ruby_version: !ruby/object:Gem::Requirement
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
none: false
|
103
|
-
requirements:
|
104
|
-
- - ">="
|
105
|
-
- !ruby/object:Gem::Version
|
106
|
-
hash: 3
|
107
|
-
segments:
|
108
|
-
- 0
|
109
|
-
version: "0"
|
84
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
85
|
+
requirements:
|
86
|
+
- - ! '>='
|
87
|
+
- !ruby/object:Gem::Version
|
88
|
+
version: '0'
|
89
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
90
|
+
requirements:
|
91
|
+
- - ! '>='
|
92
|
+
- !ruby/object:Gem::Version
|
93
|
+
version: '0'
|
110
94
|
requirements: []
|
111
|
-
|
112
95
|
rubyforge_project:
|
113
|
-
rubygems_version: 1.
|
96
|
+
rubygems_version: 2.1.2
|
114
97
|
signing_key:
|
115
|
-
specification_version:
|
116
|
-
summary: Make cookies
|
117
|
-
test_files:
|
98
|
+
specification_version: 4
|
99
|
+
summary: Make all cookies `secure` and `HttpOnly`.
|
100
|
+
test_files:
|
101
|
+
- spec/configuration_spec.rb
|
102
|
+
- spec/cookie_path_fix_spec.rb
|
118
103
|
- spec/safe_cookies_spec.rb
|
119
104
|
- spec/spec_helper.rb
|