immunio 0.15.2
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/LICENSE +234 -0
- data/README.md +147 -0
- data/bin/immunio +5 -0
- data/lib/immunio.rb +29 -0
- data/lib/immunio/agent.rb +260 -0
- data/lib/immunio/authentication.rb +96 -0
- data/lib/immunio/blocked_app.rb +38 -0
- data/lib/immunio/channel.rb +432 -0
- data/lib/immunio/cli.rb +39 -0
- data/lib/immunio/context.rb +114 -0
- data/lib/immunio/errors.rb +43 -0
- data/lib/immunio/immunio_ca.crt +45 -0
- data/lib/immunio/logger.rb +87 -0
- data/lib/immunio/plugins/action_dispatch.rb +45 -0
- data/lib/immunio/plugins/action_view.rb +431 -0
- data/lib/immunio/plugins/active_record.rb +707 -0
- data/lib/immunio/plugins/active_record_relation.rb +370 -0
- data/lib/immunio/plugins/authlogic.rb +80 -0
- data/lib/immunio/plugins/csrf.rb +24 -0
- data/lib/immunio/plugins/devise.rb +40 -0
- data/lib/immunio/plugins/environment_reporter.rb +69 -0
- data/lib/immunio/plugins/eval.rb +51 -0
- data/lib/immunio/plugins/exception_handler.rb +55 -0
- data/lib/immunio/plugins/gems_tracker.rb +5 -0
- data/lib/immunio/plugins/haml.rb +36 -0
- data/lib/immunio/plugins/http_finisher.rb +50 -0
- data/lib/immunio/plugins/http_tracker.rb +203 -0
- data/lib/immunio/plugins/io.rb +96 -0
- data/lib/immunio/plugins/redirect.rb +42 -0
- data/lib/immunio/plugins/warden.rb +66 -0
- data/lib/immunio/processor.rb +234 -0
- data/lib/immunio/rails.rb +26 -0
- data/lib/immunio/request.rb +139 -0
- data/lib/immunio/rufus_lua_ext/ref.rb +27 -0
- data/lib/immunio/rufus_lua_ext/state.rb +157 -0
- data/lib/immunio/rufus_lua_ext/table.rb +137 -0
- data/lib/immunio/rufus_lua_ext/utils.rb +13 -0
- data/lib/immunio/version.rb +5 -0
- data/lib/immunio/vm.rb +291 -0
- data/lua-hooks/ext/all.c +78 -0
- data/lua-hooks/ext/bitop/README +22 -0
- data/lua-hooks/ext/bitop/bit.c +189 -0
- data/lua-hooks/ext/extconf.rb +38 -0
- data/lua-hooks/ext/libinjection/COPYING +37 -0
- data/lua-hooks/ext/libinjection/libinjection.h +65 -0
- data/lua-hooks/ext/libinjection/libinjection_html5.c +847 -0
- data/lua-hooks/ext/libinjection/libinjection_html5.h +54 -0
- data/lua-hooks/ext/libinjection/libinjection_sqli.c +2301 -0
- data/lua-hooks/ext/libinjection/libinjection_sqli.h +295 -0
- data/lua-hooks/ext/libinjection/libinjection_sqli_data.h +9349 -0
- data/lua-hooks/ext/libinjection/libinjection_xss.c +531 -0
- data/lua-hooks/ext/libinjection/libinjection_xss.h +21 -0
- data/lua-hooks/ext/libinjection/lualib.c +109 -0
- data/lua-hooks/ext/lpeg/HISTORY +90 -0
- data/lua-hooks/ext/lpeg/lpcap.c +537 -0
- data/lua-hooks/ext/lpeg/lpcap.h +43 -0
- data/lua-hooks/ext/lpeg/lpcode.c +986 -0
- data/lua-hooks/ext/lpeg/lpcode.h +34 -0
- data/lua-hooks/ext/lpeg/lpeg-128.gif +0 -0
- data/lua-hooks/ext/lpeg/lpeg.html +1429 -0
- data/lua-hooks/ext/lpeg/lpprint.c +244 -0
- data/lua-hooks/ext/lpeg/lpprint.h +35 -0
- data/lua-hooks/ext/lpeg/lptree.c +1238 -0
- data/lua-hooks/ext/lpeg/lptree.h +77 -0
- data/lua-hooks/ext/lpeg/lptypes.h +149 -0
- data/lua-hooks/ext/lpeg/lpvm.c +355 -0
- data/lua-hooks/ext/lpeg/lpvm.h +58 -0
- data/lua-hooks/ext/lpeg/makefile +55 -0
- data/lua-hooks/ext/lpeg/re.html +498 -0
- data/lua-hooks/ext/lpeg/test.lua +1409 -0
- data/lua-hooks/ext/lua-cmsgpack/CMakeLists.txt +45 -0
- data/lua-hooks/ext/lua-cmsgpack/README.md +115 -0
- data/lua-hooks/ext/lua-cmsgpack/lua_cmsgpack.c +957 -0
- data/lua-hooks/ext/lua-cmsgpack/test.lua +570 -0
- data/lua-hooks/ext/lua-snapshot/LICENSE +7 -0
- data/lua-hooks/ext/lua-snapshot/Makefile +12 -0
- data/lua-hooks/ext/lua-snapshot/README.md +18 -0
- data/lua-hooks/ext/lua-snapshot/dump.lua +15 -0
- data/lua-hooks/ext/lua-snapshot/snapshot.c +455 -0
- data/lua-hooks/ext/lua/COPYRIGHT +34 -0
- data/lua-hooks/ext/lua/lapi.c +1087 -0
- data/lua-hooks/ext/lua/lapi.h +16 -0
- data/lua-hooks/ext/lua/lauxlib.c +652 -0
- data/lua-hooks/ext/lua/lauxlib.h +174 -0
- data/lua-hooks/ext/lua/lbaselib.c +659 -0
- data/lua-hooks/ext/lua/lcode.c +831 -0
- data/lua-hooks/ext/lua/lcode.h +76 -0
- data/lua-hooks/ext/lua/ldblib.c +398 -0
- data/lua-hooks/ext/lua/ldebug.c +638 -0
- data/lua-hooks/ext/lua/ldebug.h +33 -0
- data/lua-hooks/ext/lua/ldo.c +519 -0
- data/lua-hooks/ext/lua/ldo.h +57 -0
- data/lua-hooks/ext/lua/ldump.c +164 -0
- data/lua-hooks/ext/lua/lfunc.c +174 -0
- data/lua-hooks/ext/lua/lfunc.h +34 -0
- data/lua-hooks/ext/lua/lgc.c +710 -0
- data/lua-hooks/ext/lua/lgc.h +110 -0
- data/lua-hooks/ext/lua/linit.c +38 -0
- data/lua-hooks/ext/lua/liolib.c +556 -0
- data/lua-hooks/ext/lua/llex.c +463 -0
- data/lua-hooks/ext/lua/llex.h +81 -0
- data/lua-hooks/ext/lua/llimits.h +128 -0
- data/lua-hooks/ext/lua/lmathlib.c +263 -0
- data/lua-hooks/ext/lua/lmem.c +86 -0
- data/lua-hooks/ext/lua/lmem.h +49 -0
- data/lua-hooks/ext/lua/loadlib.c +705 -0
- data/lua-hooks/ext/lua/loadlib_rel.c +760 -0
- data/lua-hooks/ext/lua/lobject.c +214 -0
- data/lua-hooks/ext/lua/lobject.h +381 -0
- data/lua-hooks/ext/lua/lopcodes.c +102 -0
- data/lua-hooks/ext/lua/lopcodes.h +268 -0
- data/lua-hooks/ext/lua/loslib.c +243 -0
- data/lua-hooks/ext/lua/lparser.c +1339 -0
- data/lua-hooks/ext/lua/lparser.h +82 -0
- data/lua-hooks/ext/lua/lstate.c +214 -0
- data/lua-hooks/ext/lua/lstate.h +169 -0
- data/lua-hooks/ext/lua/lstring.c +111 -0
- data/lua-hooks/ext/lua/lstring.h +31 -0
- data/lua-hooks/ext/lua/lstrlib.c +871 -0
- data/lua-hooks/ext/lua/ltable.c +588 -0
- data/lua-hooks/ext/lua/ltable.h +40 -0
- data/lua-hooks/ext/lua/ltablib.c +287 -0
- data/lua-hooks/ext/lua/ltm.c +75 -0
- data/lua-hooks/ext/lua/ltm.h +54 -0
- data/lua-hooks/ext/lua/lua.c +392 -0
- data/lua-hooks/ext/lua/lua.def +131 -0
- data/lua-hooks/ext/lua/lua.h +388 -0
- data/lua-hooks/ext/lua/lua.rc +28 -0
- data/lua-hooks/ext/lua/lua_dll.rc +26 -0
- data/lua-hooks/ext/lua/luac.c +200 -0
- data/lua-hooks/ext/lua/luac.rc +1 -0
- data/lua-hooks/ext/lua/luaconf.h +763 -0
- data/lua-hooks/ext/lua/luaconf.h.in +724 -0
- data/lua-hooks/ext/lua/luaconf.h.orig +763 -0
- data/lua-hooks/ext/lua/lualib.h +53 -0
- data/lua-hooks/ext/lua/lundump.c +227 -0
- data/lua-hooks/ext/lua/lundump.h +36 -0
- data/lua-hooks/ext/lua/lvm.c +767 -0
- data/lua-hooks/ext/lua/lvm.h +36 -0
- data/lua-hooks/ext/lua/lzio.c +82 -0
- data/lua-hooks/ext/lua/lzio.h +67 -0
- data/lua-hooks/ext/lua/print.c +227 -0
- data/lua-hooks/ext/luautf8/README.md +152 -0
- data/lua-hooks/ext/luautf8/lutf8lib.c +1274 -0
- data/lua-hooks/ext/luautf8/unidata.h +3064 -0
- data/lua-hooks/lib/boot.lua +254 -0
- data/lua-hooks/lib/encode.lua +4 -0
- data/lua-hooks/lib/lexers/LICENSE +21 -0
- data/lua-hooks/lib/lexers/bash.lua +134 -0
- data/lua-hooks/lib/lexers/bash_dqstr.lua +62 -0
- data/lua-hooks/lib/lexers/css.lua +216 -0
- data/lua-hooks/lib/lexers/html.lua +106 -0
- data/lua-hooks/lib/lexers/javascript.lua +68 -0
- data/lua-hooks/lib/lexers/lexer.lua +1575 -0
- data/lua-hooks/lib/lexers/markers.lua +33 -0
- metadata +308 -0
@@ -0,0 +1,40 @@
|
|
1
|
+
# Register callbacks using Devise internals.
|
2
|
+
|
3
|
+
begin
|
4
|
+
require "devise"
|
5
|
+
rescue LoadError # rubocop:disable Lint/HandleExceptions
|
6
|
+
# Ignore
|
7
|
+
end
|
8
|
+
|
9
|
+
if defined? Devise
|
10
|
+
module Immunio
|
11
|
+
# Hook into password recovery feature to trigger the `framework_password_reset` hook.
|
12
|
+
module DeviseRecoverableHooks
|
13
|
+
extend ActiveSupport::Concern
|
14
|
+
|
15
|
+
included do
|
16
|
+
alias_method_chain :send_reset_password_instructions, :immunio
|
17
|
+
end
|
18
|
+
|
19
|
+
def send_reset_password_instructions_with_immunio(attributes={})
|
20
|
+
Request.time "plugin", "#{Module.nesting[0]}::#{__method__}" do
|
21
|
+
Immunio.logger.debug "Devise instrumentation fired for send_reset_password_instructions"
|
22
|
+
|
23
|
+
recoverable = find_or_initialize_with_errors(reset_password_keys, attributes, :not_found)
|
24
|
+
|
25
|
+
if recoverable.persisted? # Found
|
26
|
+
Immunio.password_reset user_record: recoverable, plugin: 'devise'
|
27
|
+
else
|
28
|
+
Immunio.failed_password_reset email: recoverable.email, plugin: 'devise'
|
29
|
+
end
|
30
|
+
|
31
|
+
Request.pause "plugin", "#{Module.nesting[0]}::#{__method__}" do
|
32
|
+
send_reset_password_instructions_without_immunio(attributes)
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
Devise::Models::Recoverable::ClassMethods.send :include, Immunio::DeviseRecoverableHooks
|
40
|
+
end
|
@@ -0,0 +1,69 @@
|
|
1
|
+
# Report environment data on the first request.
|
2
|
+
#
|
3
|
+
# XXX: Completely untested outside of MRI (official Ruby runtime) 1.9.3 and up!
|
4
|
+
|
5
|
+
module Immunio
|
6
|
+
class EnvironmentReporter
|
7
|
+
def initialize(app)
|
8
|
+
@app = app
|
9
|
+
|
10
|
+
# No need for mutex, reporting environment is idempotent
|
11
|
+
@reported = false
|
12
|
+
end
|
13
|
+
|
14
|
+
def call(env)
|
15
|
+
response = @app.call(env)
|
16
|
+
|
17
|
+
# Report gem usage at the end so that everything as been loaded.
|
18
|
+
Request.time "plugin", "#{Module.nesting[0]}::#{__method__}" do
|
19
|
+
report if !@reported
|
20
|
+
end
|
21
|
+
|
22
|
+
response
|
23
|
+
end
|
24
|
+
|
25
|
+
def runtime_name
|
26
|
+
if Object.const_defined?(:RUBY_DESCRIPTION) && RUBY_DESCRIPTION =~ /Enterprise/
|
27
|
+
'ree'
|
28
|
+
elsif Object.const_defined? :RUBY_ENGINE
|
29
|
+
RUBY_ENGINE.to_s
|
30
|
+
else
|
31
|
+
'unknown'
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
def runtime_version
|
36
|
+
case runtime_name
|
37
|
+
when 'ruby' then RUBY_VERSION
|
38
|
+
when 'jruby' then JRUBY_VERSION
|
39
|
+
when 'rbx' then Rubinius::VERSION
|
40
|
+
else 'unknown'
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
def report
|
45
|
+
@reported = true
|
46
|
+
|
47
|
+
info = {
|
48
|
+
runtime: {
|
49
|
+
name: runtime_name,
|
50
|
+
version: runtime_version
|
51
|
+
},
|
52
|
+
language: {
|
53
|
+
name: 'ruby',
|
54
|
+
version: RUBY_VERSION
|
55
|
+
},
|
56
|
+
platform: {
|
57
|
+
description: RUBY_PLATFORM
|
58
|
+
},
|
59
|
+
dependencies: {}
|
60
|
+
}
|
61
|
+
|
62
|
+
Gem.loaded_specs.each_pair do |name, spec|
|
63
|
+
info[:dependencies][name] = spec.version.to_s
|
64
|
+
end
|
65
|
+
|
66
|
+
Immunio.agent.environment = info
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
@@ -0,0 +1,51 @@
|
|
1
|
+
require_relative '../context'
|
2
|
+
# We use DebugInspector to execute eval() in the callers frame.
|
3
|
+
|
4
|
+
require 'binding_of_caller'
|
5
|
+
module Immunio
|
6
|
+
module KernelEvalHook
|
7
|
+
extend ActiveSupport::Concern
|
8
|
+
|
9
|
+
def self.extended(base)
|
10
|
+
base.singleton_class.alias_method_chain :eval, :immunio
|
11
|
+
end
|
12
|
+
|
13
|
+
def self.included(base)
|
14
|
+
base.alias_method_chain :eval, :immunio
|
15
|
+
end
|
16
|
+
|
17
|
+
protected
|
18
|
+
def eval_with_immunio(*args)
|
19
|
+
Immunio.logger.debug {"Eval_with_immunio with args: #{args}"}
|
20
|
+
Request.time "plugin", "#{Module.nesting[0]}::#{__method__}" do
|
21
|
+
strict_context, loose_context, stack, loose_stack = Immunio::Context.context()
|
22
|
+
Immunio.run_hook! "eval", "eval",
|
23
|
+
parameters: args,
|
24
|
+
context_key: loose_context,
|
25
|
+
stack: loose_stack
|
26
|
+
Request.pause "plugin", "#{Module.nesting[0]}::#{__method__}" do
|
27
|
+
if args.length == 1 then
|
28
|
+
# eval call did not include a binding, so we eval it in the caller's binding
|
29
|
+
# NOTE: the 7 here is the number of frames above us the caller was. Changing
|
30
|
+
# the call stack will break this...
|
31
|
+
rval = eval_without_immunio(args[0], binding.of_caller(7))
|
32
|
+
# XXX We can and should log here, but it breaks a ruby test (atom_feed_helper)
|
33
|
+
#Immunio.logger.debug {"Eval_without_immunio (binding 7) returned: #{rval.inspect}\nEval stack was: #{stack}"}
|
34
|
+
else
|
35
|
+
# eval call included a binding, so we still eval in the binding
|
36
|
+
rval = eval_without_immunio(*args)
|
37
|
+
# XXX We can and should log here, but it breaks a ruby test (atom_feed_helper)
|
38
|
+
#Immunio.logger.debug {"Eval_without_immunio returned: #{rval.inspect}"}
|
39
|
+
end
|
40
|
+
rval
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
if Immunio::agent.plugin_enabled?("eval") then
|
48
|
+
Kernel.send :include, Immunio::KernelEvalHook
|
49
|
+
Kernel.extend Immunio::KernelEvalHook
|
50
|
+
Immunio.logger.debug "Eval: All hooks installed."
|
51
|
+
end
|
@@ -0,0 +1,55 @@
|
|
1
|
+
require_relative "../blocked_app"
|
2
|
+
require_relative "../errors"
|
3
|
+
|
4
|
+
module Immunio
|
5
|
+
# Rack middleware trigger proper hook whenever an exception is raised in the app.
|
6
|
+
class ExceptionHandler
|
7
|
+
def initialize(app)
|
8
|
+
@app = app
|
9
|
+
end
|
10
|
+
|
11
|
+
def call(env)
|
12
|
+
@app.call(env)
|
13
|
+
rescue RequestBlocked
|
14
|
+
Request.time "plugin", "#{Module.nesting[0]}::#{__method__}[RequestBlocked]" do
|
15
|
+
# Avoid bubbling exception to logging middlewares.
|
16
|
+
Immunio.blocked_app.call(env)
|
17
|
+
end
|
18
|
+
rescue OverrideResponse => override
|
19
|
+
status, headers, body = Immunio.override_response.call(env, override)
|
20
|
+
|
21
|
+
Immunio.run_hook "http_tracker", "http_response_start",
|
22
|
+
status: status, headers: headers
|
23
|
+
[status, headers, body]
|
24
|
+
rescue StandardError => e
|
25
|
+
Request.time "plugin", "#{Module.nesting[0]}::#{__method__}[#{e.class}]" do
|
26
|
+
# Check for template, DB errors and such generated by the agent.
|
27
|
+
original_exception = unwrap_exception(e)
|
28
|
+
if RequestBlocked === original_exception || original_exception.message =~ /^Immunio::RequestBlocked:/
|
29
|
+
return Immunio.blocked_app.call(env)
|
30
|
+
end
|
31
|
+
if OverrideResponse === original_exception # We can't do anything with just the message:
|
32
|
+
status, headers, body = Immunio.override_response.call(env, original_exception)
|
33
|
+
|
34
|
+
Immunio.run_hook "http_tracker", "http_response_start",
|
35
|
+
status: status, headers: headers
|
36
|
+
return [status, headers, body]
|
37
|
+
end
|
38
|
+
|
39
|
+
# A real app exception, not generated by the agent.
|
40
|
+
Immunio.run_hook! "exception_handler", "exception", exception: "#{e.class.name}: #{e}"
|
41
|
+
|
42
|
+
# Re-raise
|
43
|
+
raise
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
# Unwrap the innermost original exception.
|
48
|
+
def unwrap_exception(e)
|
49
|
+
while e.respond_to? :original_exception
|
50
|
+
e = e.original_exception
|
51
|
+
end
|
52
|
+
e
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
# Haml compiles templates to something like:
|
2
|
+
#
|
3
|
+
#begin;extend Haml::Helpers;_hamlout = @haml_buffer = Haml::Buffer.new(haml_buffer, {:autoclose=>["area", "base", "basefont", "br", "col", "command", "embed", "frame", "hr", "img", "input", "isindex", "keygen", "link", "menuitem", "meta", "param", "source", "track", "wbr"], :preserve=>["textarea", "pre", "code"], :attr_wrapper=>"'", :ugly=>true, :format=>:html5, :encoding=>"UTF-8", :escape_html=>true, :escape_attrs=>true, :hyphenate_data_attrs=>true, :cdata=>false});_erbout = _hamlout.buffer;@output_buffer = output_buffer ||= ActionView::OutputBuffer.new rescue nil;;_hamlout.buffer << "<script>console.log('test')</script>\n";::Haml::Util.html_safe(_erbout);ensure;@haml_buffer = @haml_buffer.upper if @haml_buffer;end;
|
4
|
+
#
|
5
|
+
# In there is a call to ::Haml::Util.html_safe, which calls the real html_safe.
|
6
|
+
# But lots of haml code paths call ::Haml::Util.html_safe, so we can't mark it
|
7
|
+
# as safe. Instead, we need to modify the compiled template code to call the
|
8
|
+
# method in a marked-safe context.
|
9
|
+
#
|
10
|
+
# But haml doesn't make it easy, as it alias-method-chains its own methods. This
|
11
|
+
# module continues the chain to subvert the
|
12
|
+
# precompiled_method_return_value_with_haml_xss method and instead call our own
|
13
|
+
# method, which calls html_safe while in a safe context.
|
14
|
+
|
15
|
+
module Immunio
|
16
|
+
module Haml
|
17
|
+
module Compiler
|
18
|
+
extend ActiveSupport::Concern
|
19
|
+
|
20
|
+
included do
|
21
|
+
alias_method :precompiled_method_return_value_without_haml_xss_without_immunio, :precompiled_method_return_value_without_haml_xss
|
22
|
+
alias_method :precompiled_method_return_value, :precompiled_method_return_value_with_haml_xss_with_immunio
|
23
|
+
end
|
24
|
+
|
25
|
+
def self.immunio_html_safe(string)
|
26
|
+
Immunio::UnsafeBufferDetection.in_safe_context { ::Haml::Util.html_safe(string) }
|
27
|
+
end
|
28
|
+
|
29
|
+
def precompiled_method_return_value_with_haml_xss_with_immunio
|
30
|
+
"Immunio::Haml::Compiler.immunio_html_safe(#{precompiled_method_return_value_without_haml_xss_without_immunio})"
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
::Haml::Compiler.send :include, Immunio::Haml::Compiler if defined? ::Haml::Compiler
|
@@ -0,0 +1,50 @@
|
|
1
|
+
require "delegate"
|
2
|
+
|
3
|
+
module Immunio
|
4
|
+
# Rack middleware running at the very end of the stack to finish HTTP requests.
|
5
|
+
class HTTPFinisher
|
6
|
+
def initialize(app)
|
7
|
+
@app = app
|
8
|
+
end
|
9
|
+
|
10
|
+
def call(env)
|
11
|
+
status, headers, body = @app.call(env)
|
12
|
+
if Request.current
|
13
|
+
Immunio.logger.debug "Finishing request in HTTPFinisher"
|
14
|
+
[status, headers, BodyWrapper.new(body)]
|
15
|
+
else
|
16
|
+
[status, headers, body]
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
class BodyWrapper < SimpleDelegator
|
22
|
+
def initialize(body)
|
23
|
+
super body
|
24
|
+
@body = body
|
25
|
+
end
|
26
|
+
|
27
|
+
def each
|
28
|
+
begin
|
29
|
+
@body.each do |chunk|
|
30
|
+
Immunio.run_hook "http_finisher", "http_response_body_chunk",
|
31
|
+
chunk: chunk
|
32
|
+
yield chunk
|
33
|
+
end
|
34
|
+
rescue RequestBlocked
|
35
|
+
# We need to catch this exception here as access to static files is
|
36
|
+
# deferred. If one of our hooks, such as file_io throws when the Rake
|
37
|
+
# stack has unwound this far it will kill the request with a 500
|
38
|
+
yield "Request Blocked"
|
39
|
+
end
|
40
|
+
Immunio.run_hook "http_finisher", "http_request_finish"
|
41
|
+
|
42
|
+
# Reset current request
|
43
|
+
Immunio.finish_request
|
44
|
+
end
|
45
|
+
|
46
|
+
def respond_to?(*args)
|
47
|
+
@body.respond_to?(*args)
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
@@ -0,0 +1,203 @@
|
|
1
|
+
require_relative "../blocked_app"
|
2
|
+
require_relative "../errors"
|
3
|
+
|
4
|
+
module Immunio
|
5
|
+
# Rack middleware tracking HTTP requests and responses and triggers the proper hooks.
|
6
|
+
class HTTPTracker
|
7
|
+
def initialize(app)
|
8
|
+
@app = app
|
9
|
+
end
|
10
|
+
|
11
|
+
def call(env)
|
12
|
+
request = Request.new(env)
|
13
|
+
request.time "plugin", "#{Module.nesting[0]}::#{__method__}" do
|
14
|
+
Immunio.logger.debug "Creating new request in HTTPTracker"
|
15
|
+
Immunio.new_request(request)
|
16
|
+
|
17
|
+
Immunio.run_hook! "http_tracker", "http_request_start", meta_from_env(env)
|
18
|
+
|
19
|
+
env['rack.input'] = InputWrapper.new(env['rack.input'])
|
20
|
+
|
21
|
+
status, headers, body = Request.pause "plugin", "#{Module.nesting[0]}::#{__method__}" do
|
22
|
+
@app.call(env)
|
23
|
+
end
|
24
|
+
|
25
|
+
# Run hook for the session only if it was loaded
|
26
|
+
session = env["rack.session"]
|
27
|
+
if session_was_loaded?(session)
|
28
|
+
Immunio.run_hook! "http_tracker", "framework_session",
|
29
|
+
session_id: extract_session_id(session)
|
30
|
+
end
|
31
|
+
|
32
|
+
# Immunio expects response headers as a list of tuples.
|
33
|
+
list_headers = headers_to_list(headers)
|
34
|
+
|
35
|
+
result = Immunio.run_hook! "http_tracker", "http_response_start",
|
36
|
+
status: status, headers: list_headers
|
37
|
+
|
38
|
+
# If new headers are specified, convert them back to the Ruby hash format.
|
39
|
+
if result["headers"] != nil
|
40
|
+
# The new_headers completely replace the originals.
|
41
|
+
headers = list_to_headers(result["headers"].to_a)
|
42
|
+
end
|
43
|
+
|
44
|
+
# Send the response
|
45
|
+
[status, headers, body]
|
46
|
+
end
|
47
|
+
|
48
|
+
rescue RequestBlocked
|
49
|
+
Request.time "plugin", "#{Module.nesting[0]}::#{__method__}[RequestBlocked]" do
|
50
|
+
status, headers, body = Immunio.blocked_app.call(env)
|
51
|
+
# Do not allow blocking the request here
|
52
|
+
Immunio.run_hook "http_tracker", "http_response_start",
|
53
|
+
status: status, headers: headers
|
54
|
+
|
55
|
+
[status, headers, body]
|
56
|
+
end
|
57
|
+
rescue OverrideResponse => override
|
58
|
+
status, headers, body = Immunio.override_response.call(env, override)
|
59
|
+
|
60
|
+
Immunio.run_hook "http_tracker", "http_response_start",
|
61
|
+
status: status, headers: headers
|
62
|
+
[status, headers, body]
|
63
|
+
end
|
64
|
+
|
65
|
+
private
|
66
|
+
def headers_to_list(headers)
|
67
|
+
list_headers = []
|
68
|
+
headers.each do |name, value|
|
69
|
+
# Ruby treats the `Set-Cookie` header specially. If there are multiple
|
70
|
+
# Set-Cookie headers to send, they are joined into a single field,
|
71
|
+
# separated by line-feeds.
|
72
|
+
if name == "Set-Cookie"
|
73
|
+
value.split("\n").each do |cookie_val|
|
74
|
+
list_headers.push(["Set-Cookie", cookie_val])
|
75
|
+
end
|
76
|
+
else
|
77
|
+
list_headers.push([name, value])
|
78
|
+
end
|
79
|
+
end
|
80
|
+
list_headers
|
81
|
+
end
|
82
|
+
|
83
|
+
def list_to_headers(list)
|
84
|
+
new_headers = {}
|
85
|
+
list.each do |name, value|
|
86
|
+
# If this header is already in `new_headers`, append to the
|
87
|
+
# existing value with a linefeed separator.
|
88
|
+
if new_headers.has_key?(name)
|
89
|
+
new_headers[name] += ("\n" + value)
|
90
|
+
else
|
91
|
+
new_headers[name] = value
|
92
|
+
end
|
93
|
+
end
|
94
|
+
new_headers
|
95
|
+
end
|
96
|
+
|
97
|
+
def meta_from_env(env)
|
98
|
+
request = Rack::Request.new(env)
|
99
|
+
|
100
|
+
# Extract request headers from `env`.
|
101
|
+
headers = env.select { |k| k.starts_with? "HTTP_" }.
|
102
|
+
each_with_object({}) { |(k, v), h| h.store k[5..-1].downcase.tr('_', '-'), v }
|
103
|
+
|
104
|
+
# Determine scheme (http://www.rubydoc.info/github/rack/rack/master/file/SPEC)
|
105
|
+
# There are also some HTTP headers from proxies that may affect the
|
106
|
+
# scheme seen by the end user. We process those in the hooks.
|
107
|
+
scheme = env["rack.url_scheme"]
|
108
|
+
if env["HTTPS"] == "on"
|
109
|
+
# Some servers will set the HTTPS var explicity. If set, use it
|
110
|
+
scheme = "https"
|
111
|
+
end
|
112
|
+
|
113
|
+
# Determine the route name in controller#action format:
|
114
|
+
route_name = nil
|
115
|
+
|
116
|
+
if defined?(Rails.application)
|
117
|
+
begin
|
118
|
+
path = request.env['PATH_INFO']
|
119
|
+
method = request.env['REQUEST_METHOD'].downcase.to_sym
|
120
|
+
url = Rails.application.routes.recognize_path(path, method: method)
|
121
|
+
route_name = "#{url[:controller]}##{url[:action]}"
|
122
|
+
rescue ActionController::RoutingError
|
123
|
+
route_name = nil
|
124
|
+
end
|
125
|
+
end
|
126
|
+
|
127
|
+
{
|
128
|
+
protocol: env["SERVER_PROTOCOL"],
|
129
|
+
scheme: scheme,
|
130
|
+
uri: env["REQUEST_URI"],
|
131
|
+
server_name: env["SERVER_NAME"],
|
132
|
+
# SERVER_ADDR is non-standard, but rack uses it as a fallback, so
|
133
|
+
# include it here as well so we can access it from Lua.
|
134
|
+
server_addr: env["SERVER_ADDR"],
|
135
|
+
server_port: env["SERVER_PORT"],
|
136
|
+
route_name: route_name,
|
137
|
+
querystring: request.query_string,
|
138
|
+
method: request.request_method,
|
139
|
+
path: request.path_info,
|
140
|
+
socket_ip: request.ip,
|
141
|
+
socket_port: request.port,
|
142
|
+
headers: headers
|
143
|
+
}
|
144
|
+
end
|
145
|
+
|
146
|
+
def session_was_loaded?(session)
|
147
|
+
session && (session.respond_to?(:loaded?) ? session.loaded? : true)
|
148
|
+
end
|
149
|
+
|
150
|
+
def extract_session_id(session)
|
151
|
+
case
|
152
|
+
when session.respond_to?(:id)
|
153
|
+
session.id
|
154
|
+
when session[:id]
|
155
|
+
session[:id]
|
156
|
+
when session[:session_id]
|
157
|
+
session[:session_id]
|
158
|
+
else
|
159
|
+
nil
|
160
|
+
end
|
161
|
+
end
|
162
|
+
end
|
163
|
+
|
164
|
+
class InputWrapper < SimpleDelegator
|
165
|
+
def initialize(input)
|
166
|
+
super input
|
167
|
+
@input = input
|
168
|
+
end
|
169
|
+
|
170
|
+
def gets(*)
|
171
|
+
v = super
|
172
|
+
Request.time "plugin", "#{Module.nesting[0]}::#{__method__}" do
|
173
|
+
report_chunk v
|
174
|
+
end
|
175
|
+
v
|
176
|
+
end
|
177
|
+
|
178
|
+
def read(*)
|
179
|
+
v = super
|
180
|
+
Request.time "plugin", "#{Module.nesting[0]}::#{__method__}" do
|
181
|
+
report_chunk v
|
182
|
+
end
|
183
|
+
v
|
184
|
+
end
|
185
|
+
|
186
|
+
def each
|
187
|
+
Request.time "plugin", "#{Module.nesting[0]}::#{__method__}" do
|
188
|
+
@input.each do |chunk|
|
189
|
+
report_chunk chunk
|
190
|
+
Request.pause "plugin", "#{Module.nesting[0]}::#{__method__}" do
|
191
|
+
yield chunk
|
192
|
+
end
|
193
|
+
end
|
194
|
+
end
|
195
|
+
end
|
196
|
+
|
197
|
+
private
|
198
|
+
def report_chunk(chunk)
|
199
|
+
Immunio.run_hook! "http_tracker", "http_request_body_chunk",
|
200
|
+
chunk: chunk
|
201
|
+
end
|
202
|
+
end
|
203
|
+
end
|