1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147
|
# frozen_string_literal: true
require 'rack/protection'
require 'rack/utils'
require 'digest'
require 'logger'
require 'uri'
module Rack
module Protection
class Base
DEFAULT_OPTIONS = {
reaction: :default_reaction, logging: true,
message: 'Forbidden', encryptor: Digest::SHA1,
session_key: 'rack.session', status: 403,
allow_empty_referrer: true,
report_key: 'protection.failed',
html_types: %w[text/html application/xhtml text/xml application/xml]
}
attr_reader :app, :options
def self.default_options(options)
define_method(:default_options) { super().merge(options) }
end
def self.default_reaction(reaction)
alias_method(:default_reaction, reaction)
end
def default_options
DEFAULT_OPTIONS
end
def initialize(app, options = {})
@app = app
@options = default_options.merge(options)
end
def safe?(env)
%w[GET HEAD OPTIONS TRACE].include? env['REQUEST_METHOD']
end
def accepts?(env)
raise NotImplementedError, "#{self.class} implementation pending"
end
def call(env)
unless accepts? env
instrument env
result = react env
end
result or app.call(env)
end
def react(env)
result = send(options[:reaction], env)
result if (Array === result) && (result.size == 3)
end
def debug(env, message)
return unless options[:logging]
l = options[:logger] || env['rack.logger'] || ::Logger.new(env['rack.errors'])
l.debug(message)
end
def warn(env, message)
return unless options[:logging]
l = options[:logger] || env['rack.logger'] || ::Logger.new(env['rack.errors'])
l.warn(message)
end
def instrument(env)
return unless (i = options[:instrumenter])
env['rack.protection.attack'] = self.class.name.split('::').last.downcase
i.instrument('rack.protection', env)
end
def deny(env)
warn env, "attack prevented by #{self.class}"
[options[:status], { 'content-type' => 'text/plain' }, [options[:message]]]
end
def report(env)
warn env, "attack reported by #{self.class}"
env[options[:report_key]] = true
end
def session?(env)
env.include? options[:session_key]
end
def session(env)
return env[options[:session_key]] if session? env
raise "you need to set up a session middleware *before* #{self.class}"
end
def drop_session(env)
return unless session? env
session(env).clear
return if ["1", "true"].include?(ENV["RACK_PROTECTION_SILENCE_DROP_SESSION_WARNING"])
warn env, "session dropped by #{self.class}"
end
def referrer(env)
ref = env['HTTP_REFERER'].to_s
return if !options[:allow_empty_referrer] && ref.empty?
URI.parse(ref).host || Request.new(env).host
rescue URI::InvalidURIError
end
def origin(env)
env['HTTP_ORIGIN'] || env['HTTP_X_ORIGIN']
end
def random_string(secure = defined? SecureRandom)
secure ? SecureRandom.hex(16) : '%032x' % rand((2**128) - 1)
rescue NotImplementedError
random_string false
end
def encrypt(value)
options[:encryptor].hexdigest value.to_s
end
def secure_compare(a, b)
Rack::Utils.secure_compare(a.to_s, b.to_s)
end
alias default_reaction deny
def html?(headers)
return false unless (header = headers.detect { |k, _v| k.downcase == 'content-type' })
options[:html_types].include? header.last[%r{^\w+/\w+}]
end
end
end
end
|