rack-protection-monkey 1.5.3
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/License +20 -0
- data/README.md +90 -0
- data/Rakefile +48 -0
- data/lib/rack-protection.rb +1 -0
- data/lib/rack/protection.rb +40 -0
- data/lib/rack/protection/authenticity_token.rb +31 -0
- data/lib/rack/protection/base.rb +121 -0
- data/lib/rack/protection/escaped_params.rb +87 -0
- data/lib/rack/protection/form_token.rb +23 -0
- data/lib/rack/protection/frame_options.rb +37 -0
- data/lib/rack/protection/http_origin.rb +34 -0
- data/lib/rack/protection/ip_spoofing.rb +23 -0
- data/lib/rack/protection/json_csrf.rb +35 -0
- data/lib/rack/protection/path_traversal.rb +47 -0
- data/lib/rack/protection/remote_referrer.rb +20 -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 +16 -0
- data/lib/rack/protection/xss_header.rb +25 -0
- data/rack-protection.gemspec +123 -0
- data/spec/lib/rack/protection/authenticity_token_spec.rb +46 -0
- data/spec/lib/rack/protection/base_spec.rb +38 -0
- data/spec/lib/rack/protection/escaped_params_spec.rb +41 -0
- data/spec/lib/rack/protection/form_token_spec.rb +31 -0
- data/spec/lib/rack/protection/frame_options_spec.rb +37 -0
- data/spec/lib/rack/protection/http_origin_spec.rb +40 -0
- data/spec/lib/rack/protection/ip_spoofing_spec.rb +33 -0
- data/spec/lib/rack/protection/json_csrf_spec.rb +56 -0
- data/spec/lib/rack/protection/path_traversal_spec.rb +39 -0
- data/spec/lib/rack/protection/protection_spec.rb +103 -0
- data/spec/lib/rack/protection/remote_referrer_spec.rb +29 -0
- data/spec/lib/rack/protection/remote_token_spec.rb +40 -0
- data/spec/lib/rack/protection/session_hijacking_spec.rb +53 -0
- data/spec/lib/rack/protection/xss_header_spec.rb +54 -0
- data/spec/spec_helper.rb +86 -0
- data/spec/support/dummy_app.rb +7 -0
- data/spec/support/not_implemented_as_pending.rb +23 -0
- data/spec/support/rack_monkey_patches.rb +21 -0
- data/spec/support/shared_examples.rb +65 -0
- data/spec/support/spec_helpers.rb +36 -0
- metadata +180 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: f75c546a977915d22bdaf2e32eb1d903dc5e9f08
|
4
|
+
data.tar.gz: 617f770ae255145766f78710f51fce0994965b84
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 6673074dd7b647fc40803fb2cc409097205857576dbb2456c3d3fe06a676926432816c268f1dea8d10e094b81fedba213608bd1f9eed05a926d6b6ad81f236d5
|
7
|
+
data.tar.gz: 2106e5478e00ac4b20451a820327721630a18b285efe69f0ea6c39c8b6c8c0494aaffc3802984b8e6ffebd0e222dd0352403e3a9390f627cb287cc1605098fe9
|
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,90 @@
|
|
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
|
+
* `Rack::Protection::HttpOrigin`
|
47
|
+
|
48
|
+
## Cross Site Scripting
|
49
|
+
|
50
|
+
Prevented by:
|
51
|
+
|
52
|
+
* `Rack::Protection::EscapedParams` (not included by `use Rack::Protection`)
|
53
|
+
* `Rack::Protection::XSSHeader` (Internet Explorer only)
|
54
|
+
|
55
|
+
## Clickjacking
|
56
|
+
|
57
|
+
Prevented by:
|
58
|
+
|
59
|
+
* `Rack::Protection::FrameOptions`
|
60
|
+
|
61
|
+
## Directory Traversal
|
62
|
+
|
63
|
+
Prevented by:
|
64
|
+
|
65
|
+
* `Rack::Protection::PathTraversal`
|
66
|
+
|
67
|
+
## Session Hijacking
|
68
|
+
|
69
|
+
Prevented by:
|
70
|
+
|
71
|
+
* `Rack::Protection::SessionHijacking`
|
72
|
+
|
73
|
+
## IP Spoofing
|
74
|
+
|
75
|
+
Prevented by:
|
76
|
+
|
77
|
+
* `Rack::Protection::IPSpoofing`
|
78
|
+
|
79
|
+
# Installation
|
80
|
+
|
81
|
+
gem install rack-protection
|
82
|
+
|
83
|
+
# Instrumentation
|
84
|
+
|
85
|
+
Instrumentation is enabled by passing in an instrumenter as an option.
|
86
|
+
```
|
87
|
+
use Rack::Protection, instrumenter: ActiveSupport::Notifications
|
88
|
+
```
|
89
|
+
|
90
|
+
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
ADDED
@@ -0,0 +1,48 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
$LOAD_PATH.unshift File.expand_path('../lib', __FILE__)
|
3
|
+
|
4
|
+
begin
|
5
|
+
require 'bundler'
|
6
|
+
Bundler::GemHelper.install_tasks
|
7
|
+
rescue LoadError => e
|
8
|
+
$stderr.puts e
|
9
|
+
end
|
10
|
+
|
11
|
+
desc "run specs"
|
12
|
+
task(:spec) { ruby '-S rspec spec' }
|
13
|
+
|
14
|
+
desc "generate gemspec"
|
15
|
+
task 'rack-protection.gemspec' do
|
16
|
+
require 'rack/protection/version'
|
17
|
+
content = File.binread 'rack-protection.gemspec'
|
18
|
+
|
19
|
+
# fetch data
|
20
|
+
fields = {
|
21
|
+
:authors => `git shortlog -sn`.force_encoding('utf-8').scan(/[^\d\s].*/),
|
22
|
+
:email => `git shortlog -sne`.force_encoding('utf-8').scan(/[^<]+@[^>]+/),
|
23
|
+
:files => `git ls-files`.force_encoding('utf-8').split("\n").reject { |f| f =~ /^(\.|Gemfile)/ }
|
24
|
+
}
|
25
|
+
|
26
|
+
# double email :(
|
27
|
+
fields[:email].delete("konstantin.haase@gmail.com")
|
28
|
+
|
29
|
+
# insert data
|
30
|
+
fields.each do |field, values|
|
31
|
+
updated = " s.#{field} = ["
|
32
|
+
updated << values.map { |v| "\n %p" % v }.join(',')
|
33
|
+
updated << "\n ]"
|
34
|
+
content.sub!(/ s\.#{field} = \[\n( .*\n)* \]/, updated)
|
35
|
+
end
|
36
|
+
|
37
|
+
# set version
|
38
|
+
content.sub! /(s\.version.*=\s+).*/, "\\1\"#{Rack::Protection::VERSION}\""
|
39
|
+
|
40
|
+
# escape unicode
|
41
|
+
content.gsub!(/./) { |c| c.bytesize > 1 ? "\\u{#{c.codepoints.first.to_s(16)}}" : c }
|
42
|
+
|
43
|
+
File.open('rack-protection.gemspec', 'w') { |f| f << content }
|
44
|
+
end
|
45
|
+
|
46
|
+
task :gemspec => 'rack-protection.gemspec'
|
47
|
+
task :default => :spec
|
48
|
+
task :test => :spec
|
@@ -0,0 +1 @@
|
|
1
|
+
require "rack/protection"
|
@@ -0,0 +1,40 @@
|
|
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 :HttpOrigin, 'rack/protection/http_origin'
|
12
|
+
autoload :IPSpoofing, 'rack/protection/ip_spoofing'
|
13
|
+
autoload :JsonCsrf, 'rack/protection/json_csrf'
|
14
|
+
autoload :PathTraversal, 'rack/protection/path_traversal'
|
15
|
+
autoload :RemoteReferrer, 'rack/protection/remote_referrer'
|
16
|
+
autoload :RemoteToken, 'rack/protection/remote_token'
|
17
|
+
autoload :SessionHijacking, 'rack/protection/session_hijacking'
|
18
|
+
autoload :XSSHeader, 'rack/protection/xss_header'
|
19
|
+
|
20
|
+
def self.new(app, options = {})
|
21
|
+
# does not include: RemoteReferrer, AuthenticityToken and FormToken
|
22
|
+
except = Array options[:except]
|
23
|
+
use_these = Array options[:use]
|
24
|
+
Rack::Builder.new do
|
25
|
+
use ::Rack::Protection::RemoteReferrer, options if use_these.include? :remote_referrer
|
26
|
+
use ::Rack::Protection::AuthenticityToken,options if use_these.include? :authenticity_token
|
27
|
+
use ::Rack::Protection::FormToken, options if use_these.include? :form_token
|
28
|
+
use ::Rack::Protection::FrameOptions, options unless except.include? :frame_options
|
29
|
+
use ::Rack::Protection::HttpOrigin, options unless except.include? :http_origin
|
30
|
+
use ::Rack::Protection::IPSpoofing, options unless except.include? :ip_spoofing
|
31
|
+
use ::Rack::Protection::JsonCsrf, options unless except.include? :json_csrf
|
32
|
+
use ::Rack::Protection::PathTraversal, options unless except.include? :path_traversal
|
33
|
+
use ::Rack::Protection::RemoteToken, options unless except.include? :remote_token
|
34
|
+
use ::Rack::Protection::SessionHijacking, options unless except.include? :session_hijacking
|
35
|
+
use ::Rack::Protection::XSSHeader, options unless except.include? :xss_header
|
36
|
+
run app
|
37
|
+
end.to_app
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
@@ -0,0 +1,31 @@
|
|
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
|
+
#
|
15
|
+
# Options:
|
16
|
+
#
|
17
|
+
# authenticity_param: Defines the param's name that should contain the token on a request.
|
18
|
+
#
|
19
|
+
class AuthenticityToken < Base
|
20
|
+
default_options :authenticity_param => 'authenticity_token'
|
21
|
+
|
22
|
+
def accepts?(env)
|
23
|
+
session = session env
|
24
|
+
token = session[:csrf] ||= session['_csrf_token'] || random_string
|
25
|
+
safe?(env) ||
|
26
|
+
env['HTTP_X_CSRF_TOKEN'] == token ||
|
27
|
+
Request.new(env).params[options[:authenticity_param]] == token
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
@@ -0,0 +1,121 @@
|
|
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
|
+
:report_key => "protection.failed",
|
15
|
+
:html_types => %w[text/html application/xhtml]
|
16
|
+
}
|
17
|
+
|
18
|
+
attr_reader :app, :options
|
19
|
+
|
20
|
+
def self.default_options(options)
|
21
|
+
define_method(:default_options) { super().merge(options) }
|
22
|
+
end
|
23
|
+
|
24
|
+
def self.default_reaction(reaction)
|
25
|
+
alias_method(:default_reaction, reaction)
|
26
|
+
end
|
27
|
+
|
28
|
+
def default_options
|
29
|
+
DEFAULT_OPTIONS
|
30
|
+
end
|
31
|
+
|
32
|
+
def initialize(app, options = {})
|
33
|
+
@app, @options = app, default_options.merge(options)
|
34
|
+
end
|
35
|
+
|
36
|
+
def safe?(env)
|
37
|
+
%w[GET HEAD OPTIONS TRACE].include? env['REQUEST_METHOD']
|
38
|
+
end
|
39
|
+
|
40
|
+
def accepts?(env)
|
41
|
+
raise NotImplementedError, "#{self.class} implementation pending"
|
42
|
+
end
|
43
|
+
|
44
|
+
def call(env)
|
45
|
+
unless accepts? env
|
46
|
+
instrument env
|
47
|
+
result = react env
|
48
|
+
end
|
49
|
+
result or app.call(env)
|
50
|
+
end
|
51
|
+
|
52
|
+
def react(env)
|
53
|
+
result = send(options[:reaction], env)
|
54
|
+
result if Array === result and result.size == 3
|
55
|
+
end
|
56
|
+
|
57
|
+
def warn(env, message)
|
58
|
+
return unless options[:logging]
|
59
|
+
l = options[:logger] || env['rack.logger'] || ::Logger.new(env['rack.errors'])
|
60
|
+
l.warn(message)
|
61
|
+
end
|
62
|
+
|
63
|
+
def instrument(env)
|
64
|
+
return unless i = options[:instrumenter]
|
65
|
+
env['rack.protection.attack'] = self.class.name.split('::').last.downcase
|
66
|
+
i.instrument('rack.protection', env)
|
67
|
+
end
|
68
|
+
|
69
|
+
def deny(env)
|
70
|
+
warn env, "attack prevented by #{self.class}"
|
71
|
+
[options[:status], {'Content-Type' => 'text/plain'}, [options[:message]]]
|
72
|
+
end
|
73
|
+
|
74
|
+
def report(env)
|
75
|
+
warn env, "attack reported by #{self.class}"
|
76
|
+
env[options[:report_key]] = true
|
77
|
+
end
|
78
|
+
|
79
|
+
def session?(env)
|
80
|
+
env.include? options[:session_key]
|
81
|
+
end
|
82
|
+
|
83
|
+
def session(env)
|
84
|
+
return env[options[:session_key]] if session? env
|
85
|
+
fail "you need to set up a session middleware *before* #{self.class}"
|
86
|
+
end
|
87
|
+
|
88
|
+
def drop_session(env)
|
89
|
+
session(env).clear if session? env
|
90
|
+
end
|
91
|
+
|
92
|
+
def referrer(env)
|
93
|
+
ref = env['HTTP_REFERER'].to_s
|
94
|
+
return if !options[:allow_empty_referrer] and ref.empty?
|
95
|
+
URI.parse(ref).host || Request.new(env).host
|
96
|
+
rescue URI::InvalidURIError
|
97
|
+
end
|
98
|
+
|
99
|
+
def origin(env)
|
100
|
+
env['HTTP_ORIGIN'] || env['HTTP_X_ORIGIN']
|
101
|
+
end
|
102
|
+
|
103
|
+
def random_string(secure = defined? SecureRandom)
|
104
|
+
secure ? SecureRandom.hex(16) : "%032x" % rand(2**128-1)
|
105
|
+
rescue NotImplementedError
|
106
|
+
random_string false
|
107
|
+
end
|
108
|
+
|
109
|
+
def encrypt(value)
|
110
|
+
options[:encryptor].hexdigest value.to_s
|
111
|
+
end
|
112
|
+
|
113
|
+
alias default_reaction deny
|
114
|
+
|
115
|
+
def html?(headers)
|
116
|
+
return false unless header = headers.detect { |k,v| k.downcase == 'content-type' }
|
117
|
+
options[:html_types].include? header.last[/^\w+\/\w+/]
|
118
|
+
end
|
119
|
+
end
|
120
|
+
end
|
121
|
+
end
|
@@ -0,0 +1,87 @@
|
|
1
|
+
require 'rack/protection'
|
2
|
+
require 'rack/utils'
|
3
|
+
|
4
|
+
begin
|
5
|
+
require 'escape_utils'
|
6
|
+
rescue LoadError
|
7
|
+
end
|
8
|
+
|
9
|
+
module Rack
|
10
|
+
module Protection
|
11
|
+
##
|
12
|
+
# Prevented attack:: XSS
|
13
|
+
# Supported browsers:: all
|
14
|
+
# More infos:: http://en.wikipedia.org/wiki/Cross-site_scripting
|
15
|
+
#
|
16
|
+
# Automatically escapes Rack::Request#params so they can be embedded in HTML
|
17
|
+
# or JavaScript without any further issues. Calls +html_safe+ on the escaped
|
18
|
+
# strings if defined, to avoid double-escaping in Rails.
|
19
|
+
#
|
20
|
+
# Options:
|
21
|
+
# escape:: What escaping modes to use, should be Symbol or Array of Symbols.
|
22
|
+
# Available: :html (default), :javascript, :url
|
23
|
+
class EscapedParams < Base
|
24
|
+
extend Rack::Utils
|
25
|
+
|
26
|
+
class << self
|
27
|
+
alias escape_url escape
|
28
|
+
public :escape_html
|
29
|
+
end
|
30
|
+
|
31
|
+
default_options :escape => :html,
|
32
|
+
:escaper => defined?(EscapeUtils) ? EscapeUtils : self
|
33
|
+
|
34
|
+
def initialize(*)
|
35
|
+
super
|
36
|
+
|
37
|
+
modes = Array options[:escape]
|
38
|
+
@escaper = options[:escaper]
|
39
|
+
@html = modes.include? :html
|
40
|
+
@javascript = modes.include? :javascript
|
41
|
+
@url = modes.include? :url
|
42
|
+
|
43
|
+
if @javascript and not @escaper.respond_to? :escape_javascript
|
44
|
+
fail("Use EscapeUtils for JavaScript escaping.")
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
def call(env)
|
49
|
+
request = Request.new(env)
|
50
|
+
get_was = handle(request.GET)
|
51
|
+
post_was = handle(request.POST) rescue nil
|
52
|
+
app.call env
|
53
|
+
ensure
|
54
|
+
request.GET.replace get_was if get_was
|
55
|
+
request.POST.replace post_was if post_was
|
56
|
+
end
|
57
|
+
|
58
|
+
def handle(hash)
|
59
|
+
was = hash.dup
|
60
|
+
hash.replace escape(hash)
|
61
|
+
was
|
62
|
+
end
|
63
|
+
|
64
|
+
def escape(object)
|
65
|
+
case object
|
66
|
+
when Hash then escape_hash(object)
|
67
|
+
when Array then object.map { |o| escape(o) }
|
68
|
+
when String then escape_string(object)
|
69
|
+
else nil
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
def escape_hash(hash)
|
74
|
+
hash = hash.dup
|
75
|
+
hash.each { |k,v| hash[k] = escape(v) }
|
76
|
+
hash
|
77
|
+
end
|
78
|
+
|
79
|
+
def escape_string(str)
|
80
|
+
str = @escaper.escape_url(str) if @url
|
81
|
+
str = @escaper.escape_html(str) if @html
|
82
|
+
str = @escaper.escape_javascript(str) if @javascript
|
83
|
+
str
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|
87
|
+
end
|