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 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.
@@ -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
@@ -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