cookiefilter 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 661342b71d73b5849ad89f06d046a5fceec8d011
4
+ data.tar.gz: 0eae238747a3364560084e7e3173bf2d5de0a3b6
5
+ SHA512:
6
+ metadata.gz: b01b281d624b06c45693b36fa0e363336ec1be8d65b0cda7cf3b0c33735cc8db149fe9b3d2650a770df0c089f0ead44bfe9996c81a63074bb2922f47012b1790
7
+ data.tar.gz: 92aaac588a58bacbf263c5e80d3b3cd1abe827df47387c32c5e79173e750bfbbbec93ac35badc01c8504e790f876ddb83a3dc3e48bccf48181c2241d5ef45357
data/MIT-LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright 2017 Stefan Wallin
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,93 @@
1
+ [![Build Status](https://travis-ci.org/StefanWallin/cookiefilter-rails.svg?branch=master)](https://travis-ci.org/StefanWallin/cookiefilter-rails)
2
+ # Cookiefilter
3
+ Cookie Filter uses a developer defined safelist of allowed cookies and their
4
+ values to filter cookies that are not allowed by the safelist configuration.
5
+ This gem filters both incoming cookies from the browser and what cookies can
6
+ be set from rails. I want to thank MittMedia DMU for allowing me to open source
7
+ this piece of code. We are always looking for new developers ;).
8
+
9
+ ## When would I use this?
10
+ - If you want to be on top of what data is allowed to be passed to your server.
11
+ - If you have third party code executing on your first party domain(ads) that set
12
+ arbitrary cookies.
13
+ - If your amount and size of cookies can run out of control and exceed the http
14
+ header limit of 8186 bytes. (At which time certain cloud providers simply
15
+ interpret that request as an attack and serves back a white page)
16
+ - If you're already running mod_security or similar web firewalls and need to
17
+ complement with cookie filtering.
18
+
19
+ ## Performance
20
+ Measurment has shown that this filter adds less than 1ms per request.
21
+
22
+ This library has been in production on sites with 5 million weekly pageviews
23
+ for 2 years before being packaged as a gem and open sourced.
24
+
25
+ *This gem package has not been tested in production.*
26
+
27
+ ## Getting started
28
+ Install the [cookiefilter](http://rubygems.org/StefanWallin/cookiefilter) gem;
29
+ or add it to your Gemfile with bundler:
30
+
31
+ Add this line to your application's `Gemfile`:
32
+ ```ruby
33
+ gem 'cookiefilter'
34
+ ```
35
+
36
+ And then execute:
37
+ ```bash
38
+ $ bundle install
39
+ ```
40
+
41
+ Tell your app to use the Cookiefilter middleware.
42
+ For Rails 4+ apps:
43
+
44
+ ```ruby
45
+ # In config/application.rb
46
+ config.middleware.use Cookiefilter
47
+ ```
48
+
49
+ Add a `cookiefilter.rb` file to `config/initializers/`:
50
+ ```ruby
51
+ # In config/initializers/cookiefilter.rb
52
+ class Cookiefilter
53
+ def self.safelist
54
+ # This is an array of hashes. It serves as a living documentation of our
55
+ # allowed cookies and their format. Need help with regex? Visit this site:
56
+ # http://rubular.com/
57
+
58
+ # Each hash in the array is per cookie with the following format:
59
+ # description: Human readable string of what/who this cookie pertains.
60
+ # key: A regex that matches the name of the cookie or cookies matching
61
+ # the above description.
62
+ # value: A regex that validates the content of the cookie, if the regex
63
+ # is nil, no validation is done.
64
+ # sacred: This is a boolean indicating that this cookie are not to be
65
+ # removed to decrease the overall size of cookies per domain.
66
+ # It will however be removed in second run if no other options
67
+ # are left.
68
+ [
69
+ {
70
+ description: 'Rails Session Cookie',
71
+ key: /\Aproject_name_session\z/,
72
+ value: nil,
73
+ sacred: true
74
+ }
75
+ ]
76
+ end
77
+ end
78
+ ```
79
+
80
+ **Then restart your rails server.**
81
+
82
+ Happy filtering!
83
+
84
+ ## License
85
+ The gem is available as open source under the terms of the
86
+ [MIT License](http://opensource.org/licenses/MIT).
87
+
88
+ ## Contribute
89
+ All contributions are welcome, issues and PR's.
90
+ Make sure tests pass by running them like so:
91
+ ```ruby
92
+ rake test
93
+ ```
data/Rakefile ADDED
@@ -0,0 +1,33 @@
1
+ begin
2
+ require 'bundler/setup'
3
+ rescue LoadError
4
+ puts 'You must `gem install bundler` and `bundle install` to run rake tasks'
5
+ end
6
+
7
+ require 'rdoc/task'
8
+
9
+ RDoc::Task.new(:rdoc) do |rdoc|
10
+ rdoc.rdoc_dir = 'rdoc'
11
+ rdoc.title = 'Cookiefilter'
12
+ rdoc.options << '--line-numbers'
13
+ rdoc.rdoc_files.include('README.md')
14
+ rdoc.rdoc_files.include('lib/**/*.rb')
15
+ end
16
+
17
+
18
+
19
+
20
+
21
+
22
+ require 'bundler/gem_tasks'
23
+
24
+ require 'rake/testtask'
25
+
26
+ Rake::TestTask.new(:test) do |t|
27
+ t.libs << 'test'
28
+ t.pattern = 'test/**/*_test.rb'
29
+ t.verbose = false
30
+ end
31
+
32
+
33
+ task default: :test
@@ -0,0 +1,172 @@
1
+ require 'cookiefilter/utils'
2
+ require 'cookiefilter/rules'
3
+ require 'cookiefilter/errors'
4
+ require 'cookiefilter/validator'
5
+
6
+ class Cookiefilter
7
+ class << self
8
+ def filter_request_cookies(http_cookie)
9
+ # Flag by replacing with cookie emoji(\u{1F36a}) as a token to identify
10
+ # and invalidate cookies later on in is_valid_cookie!
11
+ cookies = Cookiefilter::Utils.sanitize_utf8_string(
12
+ http_cookie,
13
+ Cookiefilter::Rules::DISALLOWED_CHARS,
14
+ Cookiefilter::Rules::INVALID_TOKEN
15
+ )
16
+ cookies = Cookiefilter::Utils.parse_request_cookies(cookies)
17
+ delete, keep = Cookiefilter.validate_request_cookies(cookies)
18
+ header = Cookiefilter::Utils.construct_request_header(keep)
19
+ { delete: delete, keep: keep, header: header.strip }
20
+ end
21
+
22
+ def filter_response_cookies(host, response_header, response_cookies)
23
+ keep, delete1 = response_cookies[:keep], response_cookies[:delete]
24
+ cookies = Cookiefilter::Utils.parse_set_cookie_header(response_header)
25
+ set, delete2 = Cookiefilter.validate_response_cookies(cookies)
26
+ cookies = Cookiefilter.merge_keep_and_set(keep, set)
27
+ cookies = Cookiefilter.sort_by_size(cookies)
28
+ keepers, limited = Cookiefilter.maintain_limits(cookies)
29
+ set = Cookiefilter.filter_out_keepers(keepers)
30
+ deleted = Cookiefilter.delete_cookies(delete1 + delete2, limited)
31
+ Cookiefilter::Utils.construct_response_header(host, deleted + set)
32
+ end
33
+
34
+ def validate_request_cookies(cookies)
35
+ cookies_to_keep = cookies.select do |cookie|
36
+ is_valid_cookie!(cookie)
37
+ end
38
+ cookies_to_delete = cookies.reject do |cookie|
39
+ is_valid_cookie!(cookie)
40
+ end
41
+ [cookies_to_delete, cookies_to_keep]
42
+ end
43
+
44
+ def validate_response_cookies(cookies)
45
+ cookies_to_set, cookies_to_delete = [], []
46
+ cookies.each do |cookie|
47
+ if is_valid_cookie!(cookie)
48
+ cookies_to_set << cookie
49
+ else
50
+ cookies_to_delete << cookie
51
+ end
52
+ end
53
+ [cookies_to_set, cookies_to_delete]
54
+ end
55
+
56
+ def delete_cookies(blocked, limited)
57
+ blocked.each do |cookie|
58
+ delete(cookie)
59
+ end
60
+ limited.each do |cookie|
61
+ delete(cookie)
62
+ end
63
+ blocked + limited
64
+ end
65
+
66
+ def delete(cookie)
67
+ cookie[:options] = ' max-age=0; expires=Thu, 01 Jan 1970 00:00:00 -0000;'
68
+ cookie[:value] = 'unset'
69
+ cookie
70
+ end
71
+
72
+ def merge_keep_and_set(keep, set)
73
+ keep.each do |keeper|
74
+ keeper[:type] = :keep
75
+ end
76
+ set.each do |setter|
77
+ setter[:type] = :set
78
+ end
79
+ return keep + set
80
+ end
81
+
82
+ def maintain_limits(sized_cookies, kept_large = [], limited = [])
83
+ if Cookiefilter.cookie_size_too_large?(kept_large, sized_cookies)
84
+ removed = sized_cookies.shift if sized_cookies.count > 0
85
+ removed = kept_large.shift if removed.nil?
86
+ if removed[:sacred] && sized_cookies.count > 0
87
+ kept_large.push(removed)
88
+ else
89
+ limited.push(removed)
90
+ end
91
+ return Cookiefilter.maintain_limits(sized_cookies, kept_large, limited)
92
+ else
93
+ return [kept_large + sized_cookies, limited]
94
+ end
95
+ end
96
+
97
+ def filter_out_keepers(keepers)
98
+ keepers.select { |keeper| keeper[:type] == :set }
99
+ end
100
+
101
+ def sort_by_size(cookies)
102
+ cookies.sort! do |a, b|
103
+ b[:size] <=> a[:size]
104
+ end
105
+ end
106
+
107
+ def total_cookie_size(keep, set)
108
+ @size = 0
109
+ keep.each do |cookie|
110
+ @size += cookie[:size]
111
+ end
112
+ set.each do |cookie|
113
+ @size += cookie[:size]
114
+ end
115
+ end
116
+
117
+ def cookie_size_too_large?(keep, set)
118
+ Cookiefilter.total_cookie_size(keep, set)
119
+ @size >= Cookiefilter::Rules::COOKIE_DOMAIN_LIMIT
120
+ end
121
+
122
+ # This method will regex-check both key and value for
123
+ # this cookie against all safelisted cookies until match
124
+ # is found.
125
+ # If a valid match is found, true is returned
126
+ # Otherwise false is returned.
127
+
128
+ def is_valid_cookie!(cookie)
129
+ return false if "#{cookie[:key]}=#{cookie[:value]}".bytesize > Cookiefilter::Rules::COOKIE_LIMIT
130
+ return false if /[\u{1F36A}]/.match(cookie[:value])
131
+ Cookiefilter::Rules::safelist.each do |allowed_cookie|
132
+ if allowed_cookie[:key].match(cookie[:key])
133
+ regex = allowed_cookie[:value]
134
+
135
+ # Set sacred attribute
136
+ cookie[:sacred] = allowed_cookie[:sacred]
137
+
138
+ # Cookies with nil as value-regex, pass validation.
139
+ return true if regex.nil?
140
+
141
+ return true if regex.match(cookie[:value])
142
+
143
+ # Not matching value regex, fail validation.
144
+ return false
145
+ end
146
+ end
147
+ # Unknown cookie. Log this and fail
148
+ false
149
+ end
150
+ end # end class methods
151
+
152
+ def initialize(app)
153
+ Cookiefilter::Validator.validate_safelist(Cookiefilter::Rules.safelist)
154
+ @app = app
155
+ @size = 0
156
+ end
157
+
158
+ # entry point, executes directly after initialize
159
+ def call(env)
160
+ result = Cookiefilter.filter_request_cookies(env['HTTP_COOKIE'])
161
+ env['HTTP_COOKIE'] = result[:header]
162
+ status, headers, body = @app.call(env)
163
+ response = Rack::Response.new body, status, headers
164
+ header = Cookiefilter.filter_response_cookies(
165
+ env['HTTP_HOST'],
166
+ response.header['Set-Cookie'],
167
+ result
168
+ )
169
+ response.header['Set-Cookie'] = header
170
+ response.finish
171
+ end
172
+ end
@@ -0,0 +1,15 @@
1
+ class Cookiefilter::SafelistMissingError < StandardError
2
+ def message
3
+ 'Please define your cookie safelist as documented in Readme and then\
4
+ restart your rails server.'
5
+ end
6
+ def backtrace
7
+ []
8
+ end
9
+ end
10
+
11
+ class Cookiefilter::MalformedSafelistError < StandardError
12
+ def backtrace
13
+ []
14
+ end
15
+ end
@@ -0,0 +1,22 @@
1
+ class Cookiefilter::Rules
2
+ COOKIE_DOMAIN_LIMIT = 8186
3
+ COOKIE_LIMIT = 4093
4
+ DISALLOWED_CHARS = [
5
+ "\192", "\193", "\245", "\246", "\247", "\248", "\249",
6
+ "\250", "\251", "\252", "\253", "\254", "\255"
7
+ ]
8
+ INVALID_TOKEN = "\u{1F36A}"
9
+
10
+
11
+ def self.method_missing(m, *args, &block)
12
+ if m.to_sym == :safelist
13
+ raise Cookiefilter::SafelistMissingError
14
+ else
15
+ raise ArgumentError.new("Method `#{m}` doesn't exist.")
16
+ end
17
+ end
18
+
19
+ def self.respond_to?(method_name, include_private = false)
20
+ method_name.to_sym == :safelist || super
21
+ end
22
+ end
@@ -0,0 +1,66 @@
1
+ require 'cookiefilter/errors'
2
+
3
+ class Cookiefilter::Utils
4
+ class << self
5
+ def sanitize_utf8_string(cookie_string, banned_chars, replace_char)
6
+ result = ''
7
+ # blank? can'd handle broken utf-8 characters.
8
+ return result if cookie_string.nil?
9
+ cookie_string.each_char do |char|
10
+ if banned_chars.include? char
11
+ result << replace_char
12
+ else
13
+ result << char
14
+ end
15
+ end
16
+ result
17
+ end
18
+
19
+ def parse_request_cookies(cookie_string)
20
+ cookies = []
21
+ return cookies if cookie_string.blank?
22
+ cookie_strings = cookie_string.split(';')
23
+ cookie_strings.each do |cookie|
24
+ key, value = cookie.strip.split('=', 2)
25
+ cookies << { key: key, value: value, size: "#{key}=#{value}; ".bytesize }
26
+ end
27
+ cookies
28
+ end
29
+
30
+ def parse_set_cookie_header(header)
31
+ cookies = []
32
+ return cookies if header.blank?
33
+ header_cookies = header.split("\n")
34
+ header_cookies.each do |cookie|
35
+ key, rest = cookie.strip.split('=', 2)
36
+ value, options = rest.strip.split(';', 2)
37
+ options = '' if options.nil?
38
+ size = "#{key}=#{value}; ".bytesize
39
+ cookies << { key: key, value: value, options: options, size: size }
40
+ end
41
+ cookies
42
+ end
43
+
44
+ def construct_request_header(cookies_to_keep)
45
+ http_cookie = ''
46
+ cookies_to_keep.each do |cookie|
47
+ http_cookie << "#{cookie[:key]}=#{cookie[:value]}; "
48
+ end
49
+ # Chopping last ';' since headers does not expect it.
50
+ http_cookie.strip.chomp(';')
51
+ end
52
+
53
+ def construct_response_header(host, set)
54
+ http_cookies = []
55
+ naked_host = host.sub!(/\Awww\./, '')
56
+ set.each do |cookie|
57
+ http_cookies << "#{cookie[:key]}=#{cookie[:value]};#{cookie[:options]}"
58
+ if cookie[:value] == 'unset'
59
+ http_cookies << "#{cookie[:key]}=#{cookie[:value]};Domain=.#{naked_host};#{cookie[:options]}"
60
+ http_cookies << "#{cookie[:key]}=#{cookie[:value]};Domain=www.#{naked_host};#{cookie[:options]}"
61
+ end
62
+ end
63
+ http_cookies.join("\n")
64
+ end
65
+ end
66
+ end
@@ -0,0 +1,69 @@
1
+ class Cookiefilter::Validator
2
+ class << self
3
+ def validate_safelist(safelist)
4
+ err = Cookiefilter::MalformedSafelistError
5
+ Cookiefilter::Validator.validate_array(err, safelist)
6
+
7
+ safelist.each_index do |index|
8
+ obj = safelist[index]
9
+ Cookiefilter::Validator.validate_hash(err, index, obj)
10
+ Cookiefilter::Validator.validate_keys(err, index, obj)
11
+ Cookiefilter::Validator.validate_description(err, index, obj)
12
+ Cookiefilter::Validator.validate_key(err, index, obj)
13
+ Cookiefilter::Validator.validate_value(err, index, obj)
14
+ Cookiefilter::Validator.validate_sacred(err, index, obj)
15
+ end
16
+ true
17
+ end
18
+
19
+ def validate_array(err, list)
20
+ raise err, 'safelist method does not return Array' if list.class != Array
21
+ end
22
+
23
+ def validate_hash(err, index, obj)
24
+ if obj.class != Hash
25
+ raise err, "safelist child with index #{index}: Is not an Hash Object"
26
+ end
27
+ end
28
+
29
+ def validate_keys(err, index, obj)
30
+ %w(description key value sacred).each do |key|
31
+ unless obj.has_key?(key.to_sym)
32
+ raise err, "safelist child with index #{index}: Missing key :#{key}"
33
+ end
34
+ end
35
+ end
36
+
37
+ def validate_description(err, index, obj)
38
+ description = obj[:description]
39
+ if description.class != String
40
+ return raise err, "safelist child with index #{index}: ':description' \
41
+ must be of class String, got #{description.class}"
42
+ end
43
+ end
44
+
45
+ def validate_key(err, index, obj)
46
+ key = obj[:key]
47
+ if key.class != Regexp
48
+ return raise err, "safelist child with index #{index}: ':key' \
49
+ must be of class Regexp, got #{key.class}"
50
+ end
51
+ end
52
+
53
+ def validate_value(err, index, obj)
54
+ value = obj[:value]
55
+ if value.class != Regexp && value.class != NilClass
56
+ return raise err, "safelist child with index #{index}: ':value' \
57
+ must be of class Regexp or NilClass, got #{value.class}"
58
+ end
59
+ end
60
+
61
+ def validate_sacred(err, index, obj)
62
+ sacred = obj[:sacred]
63
+ if obj[:sacred].class != TrueClass && obj[:sacred].class != FalseClass
64
+ return raise err, "safelist child with index #{index}: ':sacred' \
65
+ must be of class TrueClass or FalseClass, got #{sacred.class}"
66
+ end
67
+ end
68
+ end
69
+ end
@@ -0,0 +1,3 @@
1
+ class Cookiefilter
2
+ VERSION = '1.0.0'
3
+ end
@@ -0,0 +1,4 @@
1
+ # desc "Explaining what the task does"
2
+ # task :cookiefilter do
3
+ # # Task goes here
4
+ # end
metadata ADDED
@@ -0,0 +1,99 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: cookiefilter
3
+ version: !ruby/object:Gem::Version
4
+ version: 1.0.0
5
+ platform: ruby
6
+ authors:
7
+ - Stefan Wallin
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2017-11-08 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: rails
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: 5.1.3
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: 5.1.3
27
+ - !ruby/object:Gem::Dependency
28
+ name: faker
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: '0'
34
+ type: :development
35
+ prerelease: false
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: byebug
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ">="
46
+ - !ruby/object:Gem::Version
47
+ version: '0'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ">="
53
+ - !ruby/object:Gem::Version
54
+ version: '0'
55
+ description: |-
56
+ Cookie Filter uses a developer defined whitelist of \
57
+ allowed cookies and their values to filter cookies that do \
58
+ not live up to the standard.
59
+ email:
60
+ - cookiefilter@stefan-wallin.se
61
+ executables: []
62
+ extensions: []
63
+ extra_rdoc_files: []
64
+ files:
65
+ - MIT-LICENSE
66
+ - README.md
67
+ - Rakefile
68
+ - lib/cookiefilter.rb
69
+ - lib/cookiefilter/errors.rb
70
+ - lib/cookiefilter/rules.rb
71
+ - lib/cookiefilter/utils.rb
72
+ - lib/cookiefilter/validator.rb
73
+ - lib/cookiefilter/version.rb
74
+ - lib/tasks/cookiefilter_tasks.rake
75
+ homepage: https://github.com/StefanWallin/cookiefilter
76
+ licenses:
77
+ - MIT
78
+ metadata: {}
79
+ post_install_message:
80
+ rdoc_options: []
81
+ require_paths:
82
+ - lib
83
+ required_ruby_version: !ruby/object:Gem::Requirement
84
+ requirements:
85
+ - - ">="
86
+ - !ruby/object:Gem::Version
87
+ version: '0'
88
+ required_rubygems_version: !ruby/object:Gem::Requirement
89
+ requirements:
90
+ - - ">="
91
+ - !ruby/object:Gem::Version
92
+ version: '0'
93
+ requirements: []
94
+ rubyforge_project:
95
+ rubygems_version: 2.6.13
96
+ signing_key:
97
+ specification_version: 4
98
+ summary: Whitelist your users cookies for your domain.
99
+ test_files: []