rack-protection 1.5.0 → 2.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 +7 -0
- data/Gemfile +13 -0
- data/License +4 -1
- data/README.md +24 -2
- data/Rakefile +21 -5
- data/lib/rack/protection/authenticity_token.rb +113 -5
- data/lib/rack/protection/base.rb +16 -2
- data/lib/rack/protection/content_security_policy.rb +80 -0
- data/lib/rack/protection/cookie_tossing.rb +75 -0
- data/lib/rack/protection/escaped_params.rb +2 -0
- data/lib/rack/protection/form_token.rb +1 -1
- data/lib/rack/protection/http_origin.rb +8 -0
- data/lib/rack/protection/json_csrf.rb +27 -5
- data/lib/rack/protection/path_traversal.rb +16 -5
- data/lib/rack/protection/remote_token.rb +1 -1
- data/lib/rack/protection/session_hijacking.rb +3 -3
- data/lib/rack/protection/strict_transport.rb +39 -0
- data/lib/rack/protection/version.rb +1 -12
- data/lib/rack/protection/xss_header.rb +1 -1
- data/lib/rack/protection.rb +38 -24
- data/rack-protection.gemspec +12 -81
- metadata +28 -77
- data/spec/authenticity_token_spec.rb +0 -33
- data/spec/escaped_params_spec.rb +0 -44
- data/spec/form_token_spec.rb +0 -33
- data/spec/frame_options_spec.rb +0 -39
- data/spec/http_origin_spec.rb +0 -38
- data/spec/ip_spoofing_spec.rb +0 -35
- data/spec/json_csrf_spec.rb +0 -44
- data/spec/path_traversal_spec.rb +0 -28
- data/spec/protection_spec.rb +0 -49
- data/spec/remote_referrer_spec.rb +0 -31
- data/spec/remote_token_spec.rb +0 -42
- data/spec/session_hijacking_spec.rb +0 -54
- data/spec/spec_helper.rb +0 -163
- data/spec/xss_header_spec.rb +0 -56
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA1:
|
|
3
|
+
metadata.gz: 60b24d006884c214484d2d6275ddfd9a09719fe6
|
|
4
|
+
data.tar.gz: b43b08983d4fc1fcf5525d28e0b704e49ee93a25
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 926c434a0da749f9e615786c4600d7f3a933454ba179b4f3671d3abeb65d843281b95038258edaac3f01ae447c032aa8844141de0a399fd5447e6cbab12deac6
|
|
7
|
+
data.tar.gz: 64892fdb92b20c537ffebbbc00d374e4d0bd07cf37dab25c21c95676b371477356c7ba3a52945e0f9a84b0db40bbd3fc964aa21a8525ed2bfac4dd3be204817f
|
data/Gemfile
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
source "http://rubygems.org"
|
|
2
|
+
# encoding: utf-8
|
|
3
|
+
|
|
4
|
+
gem 'rake'
|
|
5
|
+
|
|
6
|
+
rack_version = ENV['rack'].to_s
|
|
7
|
+
rack_version = nil if rack_version.empty? or rack_version == 'stable'
|
|
8
|
+
rack_version = {:github => 'rack/rack'} if rack_version == 'master'
|
|
9
|
+
gem 'rack', rack_version
|
|
10
|
+
|
|
11
|
+
gem 'sinatra', path: '..'
|
|
12
|
+
|
|
13
|
+
gemspec
|
data/License
CHANGED
|
@@ -1,4 +1,7 @@
|
|
|
1
|
-
|
|
1
|
+
The MIT License (MIT)
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2011-2017 Konstantin Haase
|
|
4
|
+
Copyright (c) 2015-2017 Zachary Scott
|
|
2
5
|
|
|
3
6
|
Permission is hereby granted, free of charge, to any person obtaining
|
|
4
7
|
a copy of this software and associated documentation files (the
|
data/README.md
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
|
-
|
|
1
|
+
# Rack::Protection
|
|
2
|
+
|
|
3
|
+
[](http://travis-ci.org/sinatra/rack-protection)
|
|
2
4
|
|
|
3
5
|
This gem protects against typical web attacks.
|
|
4
6
|
Should work for all Rack apps, including Rails.
|
|
@@ -50,7 +52,8 @@ Prevented by:
|
|
|
50
52
|
Prevented by:
|
|
51
53
|
|
|
52
54
|
* `Rack::Protection::EscapedParams` (not included by `use Rack::Protection`)
|
|
53
|
-
* `Rack::Protection::XSSHeader` (Internet Explorer only)
|
|
55
|
+
* `Rack::Protection::XSSHeader` (Internet Explorer and Chrome only)
|
|
56
|
+
* `Rack::Protection::ContentSecurityPolicy`
|
|
54
57
|
|
|
55
58
|
## Clickjacking
|
|
56
59
|
|
|
@@ -70,13 +73,32 @@ Prevented by:
|
|
|
70
73
|
|
|
71
74
|
* `Rack::Protection::SessionHijacking`
|
|
72
75
|
|
|
76
|
+
## Cookie Tossing
|
|
77
|
+
|
|
78
|
+
Prevented by:
|
|
79
|
+
* `Rack::Protection::CookieTossing` (not included by `use Rack::Protection`)
|
|
80
|
+
|
|
73
81
|
## IP Spoofing
|
|
74
82
|
|
|
75
83
|
Prevented by:
|
|
76
84
|
|
|
77
85
|
* `Rack::Protection::IPSpoofing`
|
|
78
86
|
|
|
87
|
+
## Helps to protect against protocol downgrade attacks and cookie hijacking
|
|
88
|
+
|
|
89
|
+
Prevented by:
|
|
90
|
+
|
|
91
|
+
* `Rack::Protection::StrictTransport` (not included by `use Rack::Protection`)
|
|
92
|
+
|
|
79
93
|
# Installation
|
|
80
94
|
|
|
81
95
|
gem install rack-protection
|
|
82
96
|
|
|
97
|
+
# Instrumentation
|
|
98
|
+
|
|
99
|
+
Instrumentation is enabled by passing in an instrumenter as an option.
|
|
100
|
+
```
|
|
101
|
+
use Rack::Protection, instrumenter: ActiveSupport::Notifications
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
The instrumenter is passed a namespace (String) and environment (Hash). The namespace is 'rack.protection' and the attack type can be obtained from the environment key 'rack.protection.attack'.
|
data/Rakefile
CHANGED
|
@@ -11,6 +11,25 @@ end
|
|
|
11
11
|
desc "run specs"
|
|
12
12
|
task(:spec) { ruby '-S rspec spec' }
|
|
13
13
|
|
|
14
|
+
namespace :doc do
|
|
15
|
+
task :readmes do
|
|
16
|
+
Dir.glob 'lib/rack/protection/*.rb' do |file|
|
|
17
|
+
excluded_files = %w[lib/rack/protection/base.rb lib/rack/protection/version.rb]
|
|
18
|
+
next if excluded_files.include?(file)
|
|
19
|
+
doc = File.read(file)[/^ module Protection(\n)+( #[^\n]*\n)*/m].scan(/^ *#(?!#) ?(.*)\n/).join("\n")
|
|
20
|
+
file = "doc/#{file[4..-4].tr("/_", "-")}.rdoc"
|
|
21
|
+
Dir.mkdir "doc" unless File.directory? "doc"
|
|
22
|
+
puts "writing #{file}"
|
|
23
|
+
File.open(file, "w") { |f| f << doc }
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
task :all => [:readmes]
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
desc "generate documentation"
|
|
31
|
+
task :doc => 'doc:all'
|
|
32
|
+
|
|
14
33
|
desc "generate gemspec"
|
|
15
34
|
task 'rack-protection.gemspec' do
|
|
16
35
|
require 'rack/protection/version'
|
|
@@ -19,13 +38,10 @@ task 'rack-protection.gemspec' do
|
|
|
19
38
|
# fetch data
|
|
20
39
|
fields = {
|
|
21
40
|
:authors => `git shortlog -sn`.force_encoding('utf-8').scan(/[^\d\s].*/),
|
|
22
|
-
:email =>
|
|
23
|
-
:files =>
|
|
41
|
+
:email => ["mail@zzak.io", "konstantin.haase@gmail.com"],
|
|
42
|
+
:files => %w(License README.md Rakefile Gemfile rack-protection.gemspec) + Dir['lib/**/*']
|
|
24
43
|
}
|
|
25
44
|
|
|
26
|
-
# double email :(
|
|
27
|
-
fields[:email].delete("konstantin.haase@gmail.com")
|
|
28
|
-
|
|
29
45
|
# insert data
|
|
30
46
|
fields.each do |field, values|
|
|
31
47
|
updated = " s.#{field} = ["
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
require 'rack/protection'
|
|
2
|
+
require 'securerandom'
|
|
3
|
+
require 'base64'
|
|
2
4
|
|
|
3
5
|
module Rack
|
|
4
6
|
module Protection
|
|
@@ -10,14 +12,120 @@ module Rack
|
|
|
10
12
|
# Only accepts unsafe HTTP requests if a given access token matches the token
|
|
11
13
|
# included in the session.
|
|
12
14
|
#
|
|
13
|
-
# Compatible with
|
|
15
|
+
# Compatible with rack-csrf.
|
|
16
|
+
#
|
|
17
|
+
# Options:
|
|
18
|
+
#
|
|
19
|
+
# authenticity_param: Defines the param's name that should contain the token on a request.
|
|
14
20
|
class AuthenticityToken < Base
|
|
21
|
+
TOKEN_LENGTH = 32
|
|
22
|
+
|
|
23
|
+
default_options :authenticity_param => 'authenticity_token',
|
|
24
|
+
:allow_if => nil
|
|
25
|
+
|
|
26
|
+
def self.token(session)
|
|
27
|
+
self.new(nil).mask_authenticity_token(session)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def self.random_token
|
|
31
|
+
SecureRandom.base64(TOKEN_LENGTH)
|
|
32
|
+
end
|
|
33
|
+
|
|
15
34
|
def accepts?(env)
|
|
16
|
-
return true if safe? env
|
|
17
35
|
session = session env
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
36
|
+
set_token(session)
|
|
37
|
+
|
|
38
|
+
safe?(env) ||
|
|
39
|
+
valid_token?(session, env['HTTP_X_CSRF_TOKEN']) ||
|
|
40
|
+
valid_token?(session, Request.new(env).params[options[:authenticity_param]]) ||
|
|
41
|
+
( options[:allow_if] && options[:allow_if].call(env) )
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def mask_authenticity_token(session)
|
|
45
|
+
token = set_token(session)
|
|
46
|
+
mask_token(token)
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
private
|
|
50
|
+
|
|
51
|
+
def set_token(session)
|
|
52
|
+
session[:csrf] ||= self.class.random_token
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# Checks the client's masked token to see if it matches the
|
|
56
|
+
# session token.
|
|
57
|
+
def valid_token?(session, token)
|
|
58
|
+
return false if token.nil? || token.empty?
|
|
59
|
+
|
|
60
|
+
begin
|
|
61
|
+
token = decode_token(token)
|
|
62
|
+
rescue ArgumentError # encoded_masked_token is invalid Base64
|
|
63
|
+
return false
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
# See if it's actually a masked token or not. We should be able
|
|
67
|
+
# to handle any unmasked tokens that we've issued without error.
|
|
68
|
+
|
|
69
|
+
if unmasked_token?(token)
|
|
70
|
+
compare_with_real_token token, session
|
|
71
|
+
|
|
72
|
+
elsif masked_token?(token)
|
|
73
|
+
token = unmask_token(token)
|
|
74
|
+
|
|
75
|
+
compare_with_real_token token, session
|
|
76
|
+
|
|
77
|
+
else
|
|
78
|
+
false # Token is malformed
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
# Creates a masked version of the authenticity token that varies
|
|
83
|
+
# on each request. The masking is used to mitigate SSL attacks
|
|
84
|
+
# like BREACH.
|
|
85
|
+
def mask_token(token)
|
|
86
|
+
token = decode_token(token)
|
|
87
|
+
one_time_pad = SecureRandom.random_bytes(token.length)
|
|
88
|
+
encrypted_token = xor_byte_strings(one_time_pad, token)
|
|
89
|
+
masked_token = one_time_pad + encrypted_token
|
|
90
|
+
encode_token(masked_token)
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
# Essentially the inverse of +mask_token+.
|
|
94
|
+
def unmask_token(masked_token)
|
|
95
|
+
# Split the token into the one-time pad and the encrypted
|
|
96
|
+
# value and decrypt it
|
|
97
|
+
token_length = masked_token.length / 2
|
|
98
|
+
one_time_pad = masked_token[0...token_length]
|
|
99
|
+
encrypted_token = masked_token[token_length..-1]
|
|
100
|
+
xor_byte_strings(one_time_pad, encrypted_token)
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def unmasked_token?(token)
|
|
104
|
+
token.length == TOKEN_LENGTH
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
def masked_token?(token)
|
|
108
|
+
token.length == TOKEN_LENGTH * 2
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
def compare_with_real_token(token, session)
|
|
112
|
+
secure_compare(token, real_token(session))
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
def real_token(session)
|
|
116
|
+
decode_token(session[:csrf])
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
def encode_token(token)
|
|
120
|
+
Base64.strict_encode64(token)
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
def decode_token(token)
|
|
124
|
+
Base64.strict_decode64(token)
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
def xor_byte_strings(s1, s2)
|
|
128
|
+
s1.bytes.zip(s2.bytes).map { |(c1,c2)| c1 ^ c2 }.pack('c*')
|
|
21
129
|
end
|
|
22
130
|
end
|
|
23
131
|
end
|
data/lib/rack/protection/base.rb
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
require 'rack/protection'
|
|
2
|
+
require 'rack/utils'
|
|
2
3
|
require 'digest'
|
|
3
4
|
require 'logger'
|
|
4
5
|
require 'uri'
|
|
@@ -43,7 +44,7 @@ module Rack
|
|
|
43
44
|
|
|
44
45
|
def call(env)
|
|
45
46
|
unless accepts? env
|
|
46
|
-
|
|
47
|
+
instrument env
|
|
47
48
|
result = react env
|
|
48
49
|
end
|
|
49
50
|
result or app.call(env)
|
|
@@ -60,11 +61,19 @@ module Rack
|
|
|
60
61
|
l.warn(message)
|
|
61
62
|
end
|
|
62
63
|
|
|
64
|
+
def instrument(env)
|
|
65
|
+
return unless i = options[:instrumenter]
|
|
66
|
+
env['rack.protection.attack'] = self.class.name.split('::').last.downcase
|
|
67
|
+
i.instrument('rack.protection', env)
|
|
68
|
+
end
|
|
69
|
+
|
|
63
70
|
def deny(env)
|
|
71
|
+
warn env, "attack prevented by #{self.class}"
|
|
64
72
|
[options[:status], {'Content-Type' => 'text/plain'}, [options[:message]]]
|
|
65
73
|
end
|
|
66
74
|
|
|
67
75
|
def report(env)
|
|
76
|
+
warn env, "attack reported by #{self.class}"
|
|
68
77
|
env[options[:report_key]] = true
|
|
69
78
|
end
|
|
70
79
|
|
|
@@ -85,6 +94,7 @@ module Rack
|
|
|
85
94
|
ref = env['HTTP_REFERER'].to_s
|
|
86
95
|
return if !options[:allow_empty_referrer] and ref.empty?
|
|
87
96
|
URI.parse(ref).host || Request.new(env).host
|
|
97
|
+
rescue URI::InvalidURIError
|
|
88
98
|
end
|
|
89
99
|
|
|
90
100
|
def origin(env)
|
|
@@ -92,7 +102,7 @@ module Rack
|
|
|
92
102
|
end
|
|
93
103
|
|
|
94
104
|
def random_string(secure = defined? SecureRandom)
|
|
95
|
-
secure ? SecureRandom.hex(
|
|
105
|
+
secure ? SecureRandom.hex(16) : "%032x" % rand(2**128-1)
|
|
96
106
|
rescue NotImplementedError
|
|
97
107
|
random_string false
|
|
98
108
|
end
|
|
@@ -101,6 +111,10 @@ module Rack
|
|
|
101
111
|
options[:encryptor].hexdigest value.to_s
|
|
102
112
|
end
|
|
103
113
|
|
|
114
|
+
def secure_compare(a, b)
|
|
115
|
+
Rack::Utils.secure_compare(a.to_s, b.to_s)
|
|
116
|
+
end
|
|
117
|
+
|
|
104
118
|
alias default_reaction deny
|
|
105
119
|
|
|
106
120
|
def html?(headers)
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
require 'rack/protection'
|
|
3
|
+
|
|
4
|
+
module Rack
|
|
5
|
+
module Protection
|
|
6
|
+
##
|
|
7
|
+
# Prevented attack:: XSS and others
|
|
8
|
+
# Supported browsers:: Firefox 23+, Safari 7+, Chrome 25+, Opera 15+
|
|
9
|
+
#
|
|
10
|
+
# Description:: Content Security Policy, a mechanism web applications
|
|
11
|
+
# can use to mitigate a broad class of content injection
|
|
12
|
+
# vulnerabilities, such as cross-site scripting (XSS).
|
|
13
|
+
# Content Security Policy is a declarative policy that lets
|
|
14
|
+
# the authors (or server administrators) of a web application
|
|
15
|
+
# inform the client about the sources from which the
|
|
16
|
+
# application expects to load resources.
|
|
17
|
+
#
|
|
18
|
+
# More info:: W3C CSP Level 1 : https://www.w3.org/TR/CSP1/ (deprecated)
|
|
19
|
+
# W3C CSP Level 2 : https://www.w3.org/TR/CSP2/ (current)
|
|
20
|
+
# W3C CSP Level 3 : https://www.w3.org/TR/CSP3/ (draft)
|
|
21
|
+
# https://developer.mozilla.org/en-US/docs/Web/Security/CSP
|
|
22
|
+
# http://caniuse.com/#search=ContentSecurityPolicy
|
|
23
|
+
# http://content-security-policy.com/
|
|
24
|
+
# https://securityheaders.io
|
|
25
|
+
# https://scotthelme.co.uk/csp-cheat-sheet/
|
|
26
|
+
# http://www.html5rocks.com/en/tutorials/security/content-security-policy/
|
|
27
|
+
#
|
|
28
|
+
# Sets the 'Content-Security-Policy[-Report-Only]' header.
|
|
29
|
+
#
|
|
30
|
+
# Options: ContentSecurityPolicy configuration is a complex topic with
|
|
31
|
+
# several levels of support that has evolved over time.
|
|
32
|
+
# See the W3C documentation and the links in the more info
|
|
33
|
+
# section for CSP usage examples and best practices. The
|
|
34
|
+
# CSP3 directives in the 'NO_ARG_DIRECTIVES' constant need to be
|
|
35
|
+
# presented in the options hash with a boolean 'true' in order
|
|
36
|
+
# to be used in a policy.
|
|
37
|
+
#
|
|
38
|
+
class ContentSecurityPolicy < Base
|
|
39
|
+
default_options default_src: :none, script_src: "'self'",
|
|
40
|
+
img_src: "'self'", style_src: "'self'",
|
|
41
|
+
connect_src: "'self'", report_only: false
|
|
42
|
+
|
|
43
|
+
DIRECTIVES = %i(base_uri child_src connect_src default_src
|
|
44
|
+
font_src form_action frame_ancestors frame_src
|
|
45
|
+
img_src manifest_src media_src object_src
|
|
46
|
+
plugin_types referrer reflected_xss report_to
|
|
47
|
+
report_uri require_sri_for sandbox script_src
|
|
48
|
+
style_src worker_src).freeze
|
|
49
|
+
|
|
50
|
+
NO_ARG_DIRECTIVES = %i(block_all_mixed_content disown_opener
|
|
51
|
+
upgrade_insecure_requests).freeze
|
|
52
|
+
|
|
53
|
+
def csp_policy
|
|
54
|
+
directives = []
|
|
55
|
+
|
|
56
|
+
DIRECTIVES.each do |d|
|
|
57
|
+
if options.key?(d)
|
|
58
|
+
directives << "#{d.to_s.sub(/_/, '-')} #{options[d]}"
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# Set these key values to boolean 'true' to include in policy
|
|
63
|
+
NO_ARG_DIRECTIVES.each do |d|
|
|
64
|
+
if options.key?(d) && options[d].is_a?(TrueClass)
|
|
65
|
+
directives << d.to_s.sub(/_/, '-')
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
directives.compact.sort.join('; ')
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def call(env)
|
|
73
|
+
status, headers, body = @app.call(env)
|
|
74
|
+
header = options[:report_only] ? 'Content-Security-Policy-Report-Only' : 'Content-Security-Policy'
|
|
75
|
+
headers[header] ||= csp_policy if html? headers
|
|
76
|
+
[status, headers, body]
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
end
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
require 'rack/protection'
|
|
2
|
+
require 'pathname'
|
|
3
|
+
|
|
4
|
+
module Rack
|
|
5
|
+
module Protection
|
|
6
|
+
##
|
|
7
|
+
# Prevented attack:: Cookie Tossing
|
|
8
|
+
# Supported browsers:: all
|
|
9
|
+
# More infos:: https://github.com/blog/1466-yummy-cookies-across-domains
|
|
10
|
+
#
|
|
11
|
+
# Does not accept HTTP requests if the HTTP_COOKIE header contains more than one
|
|
12
|
+
# session cookie. This does not protect against a cookie overflow attack.
|
|
13
|
+
#
|
|
14
|
+
# Options:
|
|
15
|
+
#
|
|
16
|
+
# session_key:: The name of the session cookie (default: 'rack.session')
|
|
17
|
+
class CookieTossing < Base
|
|
18
|
+
default_reaction :deny
|
|
19
|
+
|
|
20
|
+
def call(env)
|
|
21
|
+
status, headers, body = super
|
|
22
|
+
response = Rack::Response.new(body, status, headers)
|
|
23
|
+
request = Rack::Request.new(env)
|
|
24
|
+
remove_bad_cookies(request, response)
|
|
25
|
+
response.finish
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def accepts?(env)
|
|
29
|
+
cookie_header = env['HTTP_COOKIE']
|
|
30
|
+
cookies = Rack::Utils.parse_query(cookie_header, ';,') { |s| s }
|
|
31
|
+
cookies.each do |k, v|
|
|
32
|
+
if k == session_key && Array(v).size > 1
|
|
33
|
+
bad_cookies << k
|
|
34
|
+
elsif k != session_key && Rack::Utils.unescape(k) == session_key
|
|
35
|
+
bad_cookies << k
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
bad_cookies.empty?
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def remove_bad_cookies(request, response)
|
|
42
|
+
return if bad_cookies.empty?
|
|
43
|
+
paths = cookie_paths(request.path)
|
|
44
|
+
bad_cookies.each do |name|
|
|
45
|
+
paths.each { |path| response.set_cookie name, empty_cookie(request.host, path) }
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def redirect(env)
|
|
50
|
+
request = Request.new(env)
|
|
51
|
+
warn env, "attack prevented by #{self.class}"
|
|
52
|
+
[302, {'Content-Type' => 'text/html', 'Location' => request.path}, []]
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def bad_cookies
|
|
56
|
+
@bad_cookies ||= []
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def cookie_paths(path)
|
|
60
|
+
path = '/' if path.to_s.empty?
|
|
61
|
+
paths = []
|
|
62
|
+
Pathname.new(path).descend { |p| paths << p.to_s }
|
|
63
|
+
paths
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def empty_cookie(host, path)
|
|
67
|
+
{:value => '', :domain => host, :path => path, :expires => Time.at(0)}
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def session_key
|
|
71
|
+
@session_key ||= options[:session_key]
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
end
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
require 'rack/protection'
|
|
2
2
|
require 'rack/utils'
|
|
3
|
+
require 'tempfile'
|
|
3
4
|
|
|
4
5
|
begin
|
|
5
6
|
require 'escape_utils'
|
|
@@ -66,6 +67,7 @@ module Rack
|
|
|
66
67
|
when Hash then escape_hash(object)
|
|
67
68
|
when Array then object.map { |o| escape(o) }
|
|
68
69
|
when String then escape_string(object)
|
|
70
|
+
when Tempfile then object
|
|
69
71
|
else nil
|
|
70
72
|
end
|
|
71
73
|
end
|
|
@@ -13,7 +13,7 @@ module Rack
|
|
|
13
13
|
# This middleware is not used when using the Rack::Protection collection,
|
|
14
14
|
# since it might be a security issue, depending on your application
|
|
15
15
|
#
|
|
16
|
-
# Compatible with
|
|
16
|
+
# Compatible with rack-csrf.
|
|
17
17
|
class FormToken < AuthenticityToken
|
|
18
18
|
def accepts?(env)
|
|
19
19
|
env["HTTP_X_REQUESTED_WITH"] == "XMLHttpRequest" or super
|
|
@@ -10,9 +10,16 @@ module Rack
|
|
|
10
10
|
#
|
|
11
11
|
# Does not accept unsafe HTTP requests when value of Origin HTTP request header
|
|
12
12
|
# does not match default or whitelisted URIs.
|
|
13
|
+
#
|
|
14
|
+
# If you want to whitelist a specific domain, you can pass in as the `:origin_whitelist` option:
|
|
15
|
+
#
|
|
16
|
+
# use Rack::Protection, origin_whitelist: ["http://localhost:3000", "http://127.0.01:3000"]
|
|
17
|
+
#
|
|
18
|
+
# The `:allow_if` option can also be set to a proc to use custom allow/deny logic.
|
|
13
19
|
class HttpOrigin < Base
|
|
14
20
|
DEFAULT_PORTS = { 'http' => 80, 'https' => 443, 'coffee' => 80 }
|
|
15
21
|
default_reaction :deny
|
|
22
|
+
default_options :allow_if => nil
|
|
16
23
|
|
|
17
24
|
def base_url(env)
|
|
18
25
|
request = Rack::Request.new(env)
|
|
@@ -24,6 +31,7 @@ module Rack
|
|
|
24
31
|
return true if safe? env
|
|
25
32
|
return true unless origin = env['HTTP_ORIGIN']
|
|
26
33
|
return true if base_url(env) == origin
|
|
34
|
+
return true if options[:allow_if] && options[:allow_if].call(env)
|
|
27
35
|
Array(options[:origin_whitelist]).include? origin
|
|
28
36
|
end
|
|
29
37
|
|
|
@@ -5,21 +5,30 @@ module Rack
|
|
|
5
5
|
##
|
|
6
6
|
# Prevented attack:: CSRF
|
|
7
7
|
# Supported browsers:: all
|
|
8
|
-
# More infos:: http://flask.pocoo.org/docs/security/#json-security
|
|
8
|
+
# More infos:: http://flask.pocoo.org/docs/0.10/security/#json-security
|
|
9
|
+
# http://haacked.com/archive/2008/11/20/anatomy-of-a-subtle-json-vulnerability.aspx
|
|
9
10
|
#
|
|
10
|
-
# JSON GET APIs are vulnerable to being embedded as JavaScript
|
|
11
|
+
# JSON GET APIs are vulnerable to being embedded as JavaScript when the
|
|
11
12
|
# Array prototype has been patched to track data. Checks the referrer
|
|
12
13
|
# even on GET requests if the content type is JSON.
|
|
14
|
+
#
|
|
15
|
+
# If request includes Origin HTTP header, defers to HttpOrigin to determine
|
|
16
|
+
# if the request is safe. Please refer to the documentation for more info.
|
|
17
|
+
#
|
|
18
|
+
# The `:allow_if` option can be set to a proc to use custom allow/deny logic.
|
|
13
19
|
class JsonCsrf < Base
|
|
14
|
-
|
|
20
|
+
default_options :allow_if => nil
|
|
21
|
+
|
|
22
|
+
alias react deny
|
|
15
23
|
|
|
16
24
|
def call(env)
|
|
17
25
|
request = Request.new(env)
|
|
18
26
|
status, headers, body = app.call(env)
|
|
19
27
|
|
|
20
|
-
if has_vector?
|
|
28
|
+
if has_vector?(request, headers)
|
|
21
29
|
warn env, "attack prevented by #{self.class}"
|
|
22
|
-
|
|
30
|
+
|
|
31
|
+
react_and_close(env, body) or [status, headers, body]
|
|
23
32
|
else
|
|
24
33
|
[status, headers, body]
|
|
25
34
|
end
|
|
@@ -27,9 +36,22 @@ module Rack
|
|
|
27
36
|
|
|
28
37
|
def has_vector?(request, headers)
|
|
29
38
|
return false if request.xhr?
|
|
39
|
+
return false if options[:allow_if] && options[:allow_if].call(request.env)
|
|
30
40
|
return false unless headers['Content-Type'].to_s.split(';', 2).first =~ /^\s*application\/json\s*$/
|
|
31
41
|
origin(request.env).nil? and referrer(request.env) != request.host
|
|
32
42
|
end
|
|
43
|
+
|
|
44
|
+
def react_and_close(env, body)
|
|
45
|
+
reaction = react(env)
|
|
46
|
+
|
|
47
|
+
close_body(body) if reaction
|
|
48
|
+
|
|
49
|
+
reaction
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def close_body(body)
|
|
53
|
+
body.close if body.respond_to?(:close)
|
|
54
|
+
end
|
|
33
55
|
end
|
|
34
56
|
end
|
|
35
57
|
end
|
|
@@ -19,16 +19,27 @@ module Rack
|
|
|
19
19
|
end
|
|
20
20
|
|
|
21
21
|
def cleanup(path)
|
|
22
|
+
if path.respond_to?(:encoding)
|
|
23
|
+
# Ruby 1.9+ M17N
|
|
24
|
+
encoding = path.encoding
|
|
25
|
+
dot = '.'.encode(encoding)
|
|
26
|
+
slash = '/'.encode(encoding)
|
|
27
|
+
else
|
|
28
|
+
# Ruby 1.8
|
|
29
|
+
dot = '.'
|
|
30
|
+
slash = '/'
|
|
31
|
+
end
|
|
32
|
+
|
|
22
33
|
parts = []
|
|
23
|
-
unescaped = path.gsub(
|
|
34
|
+
unescaped = path.gsub(/%2e/i, dot).gsub(/%2f/i, slash)
|
|
24
35
|
|
|
25
|
-
unescaped.split(
|
|
26
|
-
next if part.empty? or part ==
|
|
36
|
+
unescaped.split(slash).each do |part|
|
|
37
|
+
next if part.empty? or part == dot
|
|
27
38
|
part == '..' ? parts.pop : parts << part
|
|
28
39
|
end
|
|
29
40
|
|
|
30
|
-
cleaned =
|
|
31
|
-
cleaned <<
|
|
41
|
+
cleaned = slash + parts.join(slash)
|
|
42
|
+
cleaned << slash if parts.any? and unescaped =~ %r{/\.{0,2}$}
|
|
32
43
|
cleaned
|
|
33
44
|
end
|
|
34
45
|
end
|
|
@@ -10,7 +10,7 @@ module Rack
|
|
|
10
10
|
# Only accepts unsafe HTTP requests if a given access token matches the token
|
|
11
11
|
# included in the session *or* the request comes from the same origin.
|
|
12
12
|
#
|
|
13
|
-
# Compatible with
|
|
13
|
+
# Compatible with rack-csrf.
|
|
14
14
|
class RemoteToken < AuthenticityToken
|
|
15
15
|
default_reaction :deny
|
|
16
16
|
|
|
@@ -9,12 +9,12 @@ module Rack
|
|
|
9
9
|
#
|
|
10
10
|
# Tracks request properties like the user agent in the session and empties
|
|
11
11
|
# the session if those properties change. This essentially prevents attacks
|
|
12
|
-
# from Firesheep. Since all headers taken into consideration
|
|
13
|
-
# spoofed, too, this will not prevent
|
|
12
|
+
# from Firesheep. Since all headers taken into consideration can be
|
|
13
|
+
# spoofed, too, this will not prevent determined hijacking attempts.
|
|
14
14
|
class SessionHijacking < Base
|
|
15
15
|
default_reaction :drop_session
|
|
16
16
|
default_options :tracking_key => :tracking, :encrypt_tracking => true,
|
|
17
|
-
:track => %w[HTTP_USER_AGENT
|
|
17
|
+
:track => %w[HTTP_USER_AGENT HTTP_ACCEPT_LANGUAGE]
|
|
18
18
|
|
|
19
19
|
def accepts?(env)
|
|
20
20
|
session = session env
|