rack-protection-monkey 1.5.3
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/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
|