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