cookiefilter 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
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: []