rack-protection 0.1.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.
Potentially problematic release.
This version of rack-protection might be problematic. Click here for more details.
- data/License +20 -0
- data/README.md +80 -0
- data/Rakefile +37 -0
- data/lib/rack-protection.rb +1 -0
- data/lib/rack/protection.rb +35 -0
- data/lib/rack/protection/authenticity_token.rb +24 -0
- data/lib/rack/protection/base.rb +97 -0
- data/lib/rack/protection/escaped_params.rb +61 -0
- data/lib/rack/protection/form_token.rb +23 -0
- data/lib/rack/protection/frame_options.rb +26 -0
- data/lib/rack/protection/ip_spoofing.rb +23 -0
- data/lib/rack/protection/json_csrf.rb +25 -0
- data/lib/rack/protection/path_traversal.rb +29 -0
- data/lib/rack/protection/remote_referrer.rb +23 -0
- data/lib/rack/protection/remote_token.rb +22 -0
- data/lib/rack/protection/session_hijacking.rb +36 -0
- data/lib/rack/protection/version.rb +44 -0
- data/lib/rack/protection/xss_header.rb +27 -0
- data/rack-protection.gemspec +61 -0
- data/spec/authenticity_token_spec.rb +33 -0
- data/spec/escaped_params_spec.rb +34 -0
- data/spec/form_token_spec.rb +33 -0
- data/spec/frame_options_spec.rb +24 -0
- data/spec/ip_spoofing_spec.rb +35 -0
- data/spec/json_csrf_spec.rb +23 -0
- data/spec/path_traversal_spec.rb +23 -0
- data/spec/protection_spec.rb +5 -0
- data/spec/remote_referrer_spec.rb +31 -0
- data/spec/remote_token_spec.rb +42 -0
- data/spec/session_hijacking_spec.rb +40 -0
- data/spec/spec_helper.rb +157 -0
- data/spec/xss_header_spec.rb +24 -0
- metadata +121 -0
data/License
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright (c) 2011 Konstantin Haase
|
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 NONINFRINGEMENT.
|
17
|
+
IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
|
18
|
+
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
|
19
|
+
TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
|
20
|
+
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,80 @@
|
|
1
|
+
You should use protection!
|
2
|
+
|
3
|
+
This gem protects against typical web attacks.
|
4
|
+
Should work for all Rack apps, including Rails.
|
5
|
+
|
6
|
+
# Usage
|
7
|
+
|
8
|
+
Use all protections you probably want to use:
|
9
|
+
|
10
|
+
``` ruby
|
11
|
+
# config.ru
|
12
|
+
require 'rack/protection'
|
13
|
+
use Rack::Protection
|
14
|
+
run MyApp
|
15
|
+
```
|
16
|
+
|
17
|
+
Skip a single protection middleware:
|
18
|
+
|
19
|
+
``` ruby
|
20
|
+
# config.ru
|
21
|
+
require 'rack/protection'
|
22
|
+
use Rack::Protection, :except => :path_traversal
|
23
|
+
run MyApp
|
24
|
+
```
|
25
|
+
|
26
|
+
Use a single protection middleware:
|
27
|
+
|
28
|
+
``` ruby
|
29
|
+
# config.ru
|
30
|
+
require 'rack/protection'
|
31
|
+
use Rack::Protection::AuthenticityToken
|
32
|
+
run MyApp
|
33
|
+
```
|
34
|
+
|
35
|
+
# Prevented Attacks
|
36
|
+
|
37
|
+
## Cross Site Request Forgery
|
38
|
+
|
39
|
+
Prevented by:
|
40
|
+
|
41
|
+
* `Rack::Protection::AuthenticityToken` (not included by `use Rack::Protection`)
|
42
|
+
* `Rack::Protection::FormToken` (not included by `use Rack::Protection`)
|
43
|
+
* `Rack::Protection::JsonCsrf`
|
44
|
+
* `Rack::Protection::RemoteReferrer` (not included by `use Rack::Protection`)
|
45
|
+
* `Rack::Protection::RemoteToken`
|
46
|
+
## Cross Site Scripting
|
47
|
+
|
48
|
+
Prevented by:
|
49
|
+
|
50
|
+
* `Rack::Protection::EscapedParams`
|
51
|
+
* `Rack::Protection::XssHeader` (Internet Explorer only)
|
52
|
+
|
53
|
+
## Clickjacking
|
54
|
+
|
55
|
+
Prevented by:
|
56
|
+
|
57
|
+
* `Rack::Protection::FrameOptions`
|
58
|
+
|
59
|
+
## Directory Traversal
|
60
|
+
|
61
|
+
Prevented by:
|
62
|
+
|
63
|
+
* `Rack::Protection::PathTraversal`
|
64
|
+
|
65
|
+
## Session Hijacking
|
66
|
+
|
67
|
+
Prevented by:
|
68
|
+
|
69
|
+
* `Rack::Protection::SessionHijacking`
|
70
|
+
|
71
|
+
## IP Spoofing
|
72
|
+
|
73
|
+
|
74
|
+
Prevented by:
|
75
|
+
|
76
|
+
* `Rack::Protection::IPSpoofing`
|
77
|
+
|
78
|
+
# Installation
|
79
|
+
|
80
|
+
gem install rack-protection
|
data/Rakefile
ADDED
@@ -0,0 +1,37 @@
|
|
1
|
+
$LOAD_PATH.unshift File.expand_path('../lib', __FILE__)
|
2
|
+
|
3
|
+
begin
|
4
|
+
require 'bundler'
|
5
|
+
Bundler::GemHelper.install_tasks
|
6
|
+
rescue LoadError => e
|
7
|
+
$stderr.puts e
|
8
|
+
end
|
9
|
+
|
10
|
+
desc "run specs"
|
11
|
+
task(:spec) { ruby '-S rspec spec' }
|
12
|
+
|
13
|
+
desc "generate gemspec"
|
14
|
+
task 'rack-protection.gemspec' do
|
15
|
+
require 'rack/protection/version'
|
16
|
+
content = File.read 'rack-protection.gemspec'
|
17
|
+
|
18
|
+
fields = {
|
19
|
+
:authors => `git shortlog -sn`.scan(/[^\d\s].*/),
|
20
|
+
:email => `git shortlog -sne`.scan(/[^<]+@[^>]+/),
|
21
|
+
:files => `git ls-files`.split("\n").reject { |f| f =~ /^(\.|Gemfile)/ }
|
22
|
+
}
|
23
|
+
|
24
|
+
fields.each do |field, values|
|
25
|
+
updated = " s.#{field} = ["
|
26
|
+
updated << values.map { |v| "\n %p" % v }.join(',')
|
27
|
+
updated << "\n ]"
|
28
|
+
content.sub!(/ s\.#{field} = \[\n( .*\n)* \]/, updated)
|
29
|
+
end
|
30
|
+
|
31
|
+
content.sub! /(s\.version.*=\s+).*/, "\\1\"#{Rack::Protection::VERSION}\""
|
32
|
+
File.open('rack-protection.gemspec', 'w') { |f| f << content }
|
33
|
+
end
|
34
|
+
|
35
|
+
task :gemspec => 'rack-protection.gemspec'
|
36
|
+
task :default => :spec
|
37
|
+
task :test => :spec
|
@@ -0,0 +1 @@
|
|
1
|
+
require "rack/protection"
|
@@ -0,0 +1,35 @@
|
|
1
|
+
require 'rack/protection/version'
|
2
|
+
require 'rack'
|
3
|
+
|
4
|
+
module Rack
|
5
|
+
module Protection
|
6
|
+
autoload :AuthenticityToken, 'rack/protection/authenticity_token'
|
7
|
+
autoload :Base, 'rack/protection/base'
|
8
|
+
autoload :EscapedParams, 'rack/protection/escaped_params'
|
9
|
+
autoload :FormToken, 'rack/protection/form_token'
|
10
|
+
autoload :FrameOptions, 'rack/protection/frame_options'
|
11
|
+
autoload :IPSpoofing, 'rack/protection/ip_spoofing'
|
12
|
+
autoload :JsonCsrf, 'rack/protection/json_csrf'
|
13
|
+
autoload :PathTraversal, 'rack/protection/path_traversal'
|
14
|
+
autoload :RemoteReferrer, 'rack/protection/remote_referrer'
|
15
|
+
autoload :RemoteToken, 'rack/protection/remote_token'
|
16
|
+
autoload :SessionHijacking, 'rack/protection/session_hijacking'
|
17
|
+
autoload :XSSHeader, 'rack/protection/xss_header'
|
18
|
+
|
19
|
+
def self.new(app, options = {})
|
20
|
+
# does not include: RemoteReferrer, AuthenticityToken and FormToken
|
21
|
+
except = Array options[:except]
|
22
|
+
Rack::Builder.new do
|
23
|
+
use EscapedParams, options unless except.include? :escaped_params
|
24
|
+
use FrameOptions, options unless except.include? :frame_options
|
25
|
+
use IPSpoofing, options unless except.include? :ip_spoofing
|
26
|
+
use JsonCsrf, options unless except.include? :json_csrf
|
27
|
+
use PathTraversal, options unless except.include? :path_traversal
|
28
|
+
use RemoteToken, options unless except.include? :remote_token
|
29
|
+
use SessionHijacking, options unless except.include? :session_hijacking
|
30
|
+
use XSSHeader, options unless except.include? :xss_header
|
31
|
+
run app
|
32
|
+
end.to_app
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
require 'rack/protection'
|
2
|
+
|
3
|
+
module Rack
|
4
|
+
module Protection
|
5
|
+
##
|
6
|
+
# Prevented attack:: CSRF
|
7
|
+
# Supported browsers:: all
|
8
|
+
# More infos:: http://en.wikipedia.org/wiki/Cross-site_request_forgery
|
9
|
+
#
|
10
|
+
# Only accepts unsafe HTTP requests if a given access token matches the token
|
11
|
+
# included in the session.
|
12
|
+
#
|
13
|
+
# Compatible with Rails and rack-csrf.
|
14
|
+
class AuthenticityToken < Base
|
15
|
+
def accepts?(env)
|
16
|
+
return true if safe? env
|
17
|
+
session = session env
|
18
|
+
token = session[:csrf] ||= session['_csrf_token'] || random_string
|
19
|
+
env['HTTP_X_CSRF_TOKEN'] == token or
|
20
|
+
Request.new(env).params['authenticity_token'] == token
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,97 @@
|
|
1
|
+
require 'rack/protection'
|
2
|
+
require 'digest'
|
3
|
+
require 'logger'
|
4
|
+
require 'uri'
|
5
|
+
|
6
|
+
module Rack
|
7
|
+
module Protection
|
8
|
+
class Base
|
9
|
+
DEFAULT_OPTIONS = {
|
10
|
+
:reaction => :default_reaction, :logging => true,
|
11
|
+
:message => 'Forbidden', :encryptor => Digest::SHA1,
|
12
|
+
:session_key => 'rack.session', :status => 403,
|
13
|
+
:allow_empty_referrer => true
|
14
|
+
}
|
15
|
+
|
16
|
+
attr_reader :app, :options
|
17
|
+
|
18
|
+
def self.default_options(options)
|
19
|
+
define_method(:default_options) { super().merge(options) }
|
20
|
+
end
|
21
|
+
|
22
|
+
def self.default_reaction(reaction)
|
23
|
+
alias_method(:default_reaction, reaction)
|
24
|
+
end
|
25
|
+
|
26
|
+
def default_options
|
27
|
+
DEFAULT_OPTIONS
|
28
|
+
end
|
29
|
+
|
30
|
+
def initialize(app, options = {})
|
31
|
+
@app, @options = app, default_options.merge(options)
|
32
|
+
end
|
33
|
+
|
34
|
+
def safe?(env)
|
35
|
+
%w[GET HEAD OPTIONS TRACE].include? env['REQUEST_METHOD']
|
36
|
+
end
|
37
|
+
|
38
|
+
def accepts?(env)
|
39
|
+
raise NotImplementedError, "#{self.class} implementation pending"
|
40
|
+
end
|
41
|
+
|
42
|
+
def call(env)
|
43
|
+
unless accepts? env
|
44
|
+
warn env, "attack prevented by #{self.class}"
|
45
|
+
result = react env
|
46
|
+
end
|
47
|
+
result or app.call(env)
|
48
|
+
end
|
49
|
+
|
50
|
+
def react(env)
|
51
|
+
result = send(options[:reaction], env)
|
52
|
+
result if Array === result and result.size == 3
|
53
|
+
end
|
54
|
+
|
55
|
+
def warn(env, message)
|
56
|
+
return unless options[:logging]
|
57
|
+
l = options[:logger] || env['rack.logger'] || ::Logger.new(env['rack.errors'])
|
58
|
+
l.warn(message)
|
59
|
+
end
|
60
|
+
|
61
|
+
def deny(env)
|
62
|
+
[options[:status], {'Content-Type' => 'text/plain'}, [options[:message]]]
|
63
|
+
end
|
64
|
+
|
65
|
+
def session?(env)
|
66
|
+
env.include? options[:session_key]
|
67
|
+
end
|
68
|
+
|
69
|
+
def session(env)
|
70
|
+
return env[options[:session_key]] if session? env
|
71
|
+
fail "you need to set up a session middleware *before* #{self.class}"
|
72
|
+
end
|
73
|
+
|
74
|
+
def drop_session(env)
|
75
|
+
session(env).clear if session? env
|
76
|
+
end
|
77
|
+
|
78
|
+
def referrer(env)
|
79
|
+
ref = env['HTTP_REFERER'].to_s
|
80
|
+
return if !options[:allow_empty_referrer] and ref.empty?
|
81
|
+
URI.parse(ref).host || Request.new(env).host
|
82
|
+
end
|
83
|
+
|
84
|
+
def random_string(secure = defined? SecureRandom)
|
85
|
+
secure ? SecureRandom.hex(32) : "%032x" % rand(2**128-1)
|
86
|
+
rescue NotImpelentedError
|
87
|
+
random_string false
|
88
|
+
end
|
89
|
+
|
90
|
+
def encrypt(value)
|
91
|
+
options[:encryptor].hexdigest value.to_s
|
92
|
+
end
|
93
|
+
|
94
|
+
alias default_reaction deny
|
95
|
+
end
|
96
|
+
end
|
97
|
+
end
|
@@ -0,0 +1,61 @@
|
|
1
|
+
require 'rack/protection'
|
2
|
+
require 'escape_utils'
|
3
|
+
|
4
|
+
module Rack
|
5
|
+
module Protection
|
6
|
+
##
|
7
|
+
# Prevented attack:: XSS
|
8
|
+
# Supported browsers:: all
|
9
|
+
# More infos:: http://en.wikipedia.org/wiki/Cross-site_scripting
|
10
|
+
#
|
11
|
+
# Automatically escapes Rack::Request#params so they can be embedded in HTML
|
12
|
+
# or JavaScript without any further issues. Calls +html_safe+ on the escaped
|
13
|
+
# strings if defined, to avoid double-escaping in Rails.
|
14
|
+
#
|
15
|
+
# Options:
|
16
|
+
# escape:: What escaping modes to use, should be Symbol or Array of Symbols.
|
17
|
+
# Available: :html (default), :javascript, :url
|
18
|
+
class EscapedParams < Base
|
19
|
+
default_options :escape => :html
|
20
|
+
|
21
|
+
def initialize(*)
|
22
|
+
super
|
23
|
+
modes = Array options[:escape]
|
24
|
+
code = "def self.escape_string(str) %s end"
|
25
|
+
modes.each { |m| code %= "EscapeUtils.escape_#{m}(%s)"}
|
26
|
+
eval code % 'str'
|
27
|
+
end
|
28
|
+
|
29
|
+
def call(env)
|
30
|
+
request = Request.new(env)
|
31
|
+
get_was = handle(request.GET)
|
32
|
+
post_was = handle(request.POST) rescue nil
|
33
|
+
app.call env
|
34
|
+
ensure
|
35
|
+
request.GET.replace get_was
|
36
|
+
request.POST.replace post_was if post_was
|
37
|
+
end
|
38
|
+
|
39
|
+
def handle(hash)
|
40
|
+
was = hash.dup
|
41
|
+
hash.replace escape(hash)
|
42
|
+
was
|
43
|
+
end
|
44
|
+
|
45
|
+
def escape(object)
|
46
|
+
case object
|
47
|
+
when Hash then escape_hash(object)
|
48
|
+
when Array then object.map { |o| escape(o) }
|
49
|
+
when String then escape_string(object)
|
50
|
+
else raise ArgumentError, "cannot escape #{object.inspect}"
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
def escape_hash(hash)
|
55
|
+
hash = hash.dup
|
56
|
+
hash.each { |k,v| hash[k] = escape(v) }
|
57
|
+
hash
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
require 'rack/protection'
|
2
|
+
|
3
|
+
module Rack
|
4
|
+
module Protection
|
5
|
+
##
|
6
|
+
# Prevented attack:: CSRF
|
7
|
+
# Supported browsers:: all
|
8
|
+
# More infos:: http://en.wikipedia.org/wiki/Cross-site_request_forgery
|
9
|
+
#
|
10
|
+
# Only accepts submitted forms if a given access token matches the token
|
11
|
+
# included in the session. Does not expect such a token from Ajax request.
|
12
|
+
#
|
13
|
+
# This middleware is not used when using the Rack::Protection collection,
|
14
|
+
# since it might be a security issue, depending on your application
|
15
|
+
#
|
16
|
+
# Compatible with Rails and rack-csrf.
|
17
|
+
class FormToken < AuthenticityToken
|
18
|
+
def accepts?(env)
|
19
|
+
env["HTTP_X_REQUESTED_WITH"] == "XMLHttpRequest" or super
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
require 'rack/protection'
|
2
|
+
|
3
|
+
module Rack
|
4
|
+
module Protection
|
5
|
+
##
|
6
|
+
# Prevented attack:: Clickjacking
|
7
|
+
# Supported browsers:: Internet Explorer 8, Firefox 3.6.9, Opera 10.50,
|
8
|
+
# Safari 4.0, Chrome 4.1.249.1042 and later
|
9
|
+
# More infos:: https://developer.mozilla.org/en/The_X-FRAME-OPTIONS_response_header
|
10
|
+
#
|
11
|
+
# Sets X-Frame-Options header to tell the browser avoid embedding the page
|
12
|
+
# in a frame.
|
13
|
+
#
|
14
|
+
# Options:
|
15
|
+
#
|
16
|
+
# frame_options:: Defines who should be allowed to embed the page in a
|
17
|
+
# frame. Use :deny to forbid any embedding, :sameorigin
|
18
|
+
# to allow embedding from the same origin (default).
|
19
|
+
class FrameOptions < XSSHeader
|
20
|
+
default_options :frame_options => :sameorigin
|
21
|
+
def header
|
22
|
+
{ 'X-Frame-Options' => options[:frame_options].to_s }
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
require 'rack/protection'
|
2
|
+
|
3
|
+
module Rack
|
4
|
+
module Protection
|
5
|
+
##
|
6
|
+
# Prevented attack:: IP spoofing
|
7
|
+
# Supported browsers:: all
|
8
|
+
# More infos:: http://blog.c22.cc/2011/04/22/surveymonkey-ip-spoofing/
|
9
|
+
#
|
10
|
+
# Detect (some) IP spoofing attacks.
|
11
|
+
class IPSpoofing < Base
|
12
|
+
default_reaction :deny
|
13
|
+
|
14
|
+
def accepts?(env)
|
15
|
+
return true unless env.include? 'HTTP_X_FORWARDED_FOR'
|
16
|
+
ips = env['HTTP_X_FORWARDED_FOR'].split /\s*,\s*/
|
17
|
+
return false if env.include? 'HTTP_CLIENT_IP' and not ips.include? env['HTTP_CLIENT_IP']
|
18
|
+
return false if env.include? 'HTTP_X_REAL_IP' and not ips.include? env['HTTP_X_REAL_IP']
|
19
|
+
true
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
require 'rack/protection'
|
2
|
+
|
3
|
+
module Rack
|
4
|
+
module Protection
|
5
|
+
##
|
6
|
+
# Prevented attack:: CSRF
|
7
|
+
# Supported browsers:: all
|
8
|
+
# More infos:: http://flask.pocoo.org/docs/security/#json-security
|
9
|
+
#
|
10
|
+
# JSON GET APIs are volnurable to being embedded as JavaScript while the
|
11
|
+
# Array prototype has been patched to track data. Checks the referrer
|
12
|
+
# even on GET requests if the content type is JSON.
|
13
|
+
class JsonCsrf < Base
|
14
|
+
default_reaction :deny
|
15
|
+
|
16
|
+
def call(env)
|
17
|
+
status, headers, body = app.call(env)
|
18
|
+
if headers['Content-Type'].to_s.split(';', 2).first.strip == 'application/json'
|
19
|
+
result = react(env) if referrer(env) != Request.new(env).host
|
20
|
+
end
|
21
|
+
result or [status, headers, body]
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|