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,707 @@
|
|
1
|
+
require_relative "../context"
|
2
|
+
require_relative "active_record_relation.rb"
|
3
|
+
|
4
|
+
module Immunio
|
5
|
+
# Since every value that will be escaped is very likely to be param passed to a SQL query,
|
6
|
+
# we hook to the method escaping the values.
|
7
|
+
#
|
8
|
+
# Params are then sent to the QueryTracker which will take care of matching the params to the query.
|
9
|
+
module QuotingHooks
|
10
|
+
extend ActiveSupport::Concern
|
11
|
+
|
12
|
+
included do
|
13
|
+
alias_method_chain :quote, :immunio if method_defined? :quote
|
14
|
+
end
|
15
|
+
|
16
|
+
IGNORED_TYPES = [TrueClass, FalseClass, NilClass, Fixnum, Bignum, Float].freeze
|
17
|
+
|
18
|
+
def quote_with_immunio(value, column = nil)
|
19
|
+
Request.time "plugin", "#{Module.nesting[0]}::#{__method__}" do
|
20
|
+
if column
|
21
|
+
column_name = column.name
|
22
|
+
else
|
23
|
+
column_name = nil
|
24
|
+
end
|
25
|
+
|
26
|
+
# Ignored empty strings and values that can't contain injections.
|
27
|
+
unless value.blank? || IGNORED_TYPES.include?(value.class)
|
28
|
+
QueryTracker.instance.add_param column_name, value.to_s, object_id
|
29
|
+
end
|
30
|
+
|
31
|
+
Request.pause "plugin", "#{Module.nesting[0]}::#{__method__}" do
|
32
|
+
quote_without_immunio(value, column)
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
# There is one place where a statement may be quoted without going through the
|
39
|
+
# quote method. This occurs when a where statement is given an array, like:
|
40
|
+
#
|
41
|
+
# Users.where(["email LIKE %s", "bob@example.com"])
|
42
|
+
#
|
43
|
+
# The first value is a sprintf format string and the rest are values
|
44
|
+
# interpolated into it. This triggers a call into sanitize_sql_array, which
|
45
|
+
# will pass the values through quote_string, but only if the first value is
|
46
|
+
# not a Hash and the statement does not include '?' placeholders. Otherwise,
|
47
|
+
# different interpolation and quoting mechanisms are used.
|
48
|
+
#
|
49
|
+
# The above has been verified to be the case from Rails 3.0 to 4.2.
|
50
|
+
module SanitizeHooks
|
51
|
+
extend ActiveSupport::Concern
|
52
|
+
|
53
|
+
included do |base|
|
54
|
+
base.class_eval do
|
55
|
+
def sanitize_sql_array_with_immunio(ary)
|
56
|
+
Request.time "plugin", "#{Module.nesting[0]}::#{__method__}" do
|
57
|
+
statement, *values = ary
|
58
|
+
|
59
|
+
# Check if rails will use some other mechanism for quoting
|
60
|
+
unless (values.first.is_a?(Hash) && statement =~ /:\w+/) ||
|
61
|
+
(statement.include?('?')) ||
|
62
|
+
(statement.blank?)
|
63
|
+
# Rails is going to use quote_string, so handle parameters
|
64
|
+
values.each { |value| QueryTracker.instance.add_param nil, value, connection.object_id }
|
65
|
+
end
|
66
|
+
|
67
|
+
Request.pause "plugin", "#{Module.nesting[0]}::#{__method__}" do
|
68
|
+
sanitize_sql_array_without_immunio ary
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
72
|
+
alias_method_chain :sanitize_sql_array, :immunio
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
module ArelToSqlHooks
|
78
|
+
extend ActiveSupport::Concern
|
79
|
+
|
80
|
+
included do
|
81
|
+
alias_method_chain :accept, :immunio if method_defined? :accept
|
82
|
+
end
|
83
|
+
|
84
|
+
def accept_with_immunio(object, *args)
|
85
|
+
Request.time "plugin", "#{Module.nesting[0]}::#{__method__}" do
|
86
|
+
visitor = ArelNodeVisitor.new(@connection.object_id)
|
87
|
+
visitor.accept(object)
|
88
|
+
end
|
89
|
+
|
90
|
+
accept_without_immunio(object, *args)
|
91
|
+
end
|
92
|
+
end
|
93
|
+
|
94
|
+
# Arel AST visitor to collect params and modifiers. Based on Arel::Visitors::DepthFirst.
|
95
|
+
#
|
96
|
+
# Whenever a query is built in Rails, a tree of Ruby objects (AST) is built to represent that query.
|
97
|
+
# Arel (the engine building this tree) uses a Visitor to build the SQL statement represented by tree.
|
98
|
+
# We use the same base class as Arel, but instead of building some SQL, we track the params and modifiers.
|
99
|
+
#
|
100
|
+
# See http://en.wikipedia.org/wiki/Visitor_pattern
|
101
|
+
class ArelNodeVisitor < Arel::Visitors::Visitor
|
102
|
+
# Only accepts statements to avoid duplicates in params if branches are visited multiple times.
|
103
|
+
VISITABLES = [Arel::Nodes::SelectStatement, Arel::Nodes::InsertStatement,
|
104
|
+
Arel::Nodes::UpdateStatement, Arel::Nodes::DeleteStatement].freeze
|
105
|
+
|
106
|
+
IGNORED_EXPRESSIONS = ["", "*", "1 AS one"].freeze
|
107
|
+
|
108
|
+
# Copied from Arel::Visitors::Visitor to ensure the cached dispatch table isn't shared with
|
109
|
+
# other Arel visitors under Rails 3.2.
|
110
|
+
DISPATCH = Hash.new do |hash, klass|
|
111
|
+
hash[klass] = "visit_#{(klass.name || '').gsub('::', '_')}"
|
112
|
+
end
|
113
|
+
|
114
|
+
def initialize(connection_id)
|
115
|
+
@connection_id = connection_id
|
116
|
+
super()
|
117
|
+
end
|
118
|
+
|
119
|
+
# Entry point into the visitor.
|
120
|
+
def accept(object)
|
121
|
+
if VISITABLES.include?(object.class)
|
122
|
+
visit object, {}
|
123
|
+
end
|
124
|
+
end
|
125
|
+
|
126
|
+
def dispatch
|
127
|
+
DISPATCH
|
128
|
+
end
|
129
|
+
|
130
|
+
private
|
131
|
+
# Backported from Arel 6 Visitor::Reduce, as Arel 3 (Used in Rails 3.2) doesn't have the Reduce visitor.
|
132
|
+
def visit object, collector, opts={}
|
133
|
+
send dispatch[object.class], object, collector, opts
|
134
|
+
rescue NoMethodError => e
|
135
|
+
raise e if respond_to?(dispatch[object.class], true)
|
136
|
+
superklass = object.class.ancestors.find { |klass|
|
137
|
+
respond_to?(dispatch[klass], true)
|
138
|
+
}
|
139
|
+
raise(TypeError, "Cannot visit #{object.class}") unless superklass
|
140
|
+
dispatch[object.class] = dispatch[superklass]
|
141
|
+
retry
|
142
|
+
end
|
143
|
+
|
144
|
+
# When an unsafe node is visited. Track the param or modifier.
|
145
|
+
def unsafe(o, context, _opts)
|
146
|
+
unless o.class == String
|
147
|
+
QueryTracker.instance.add_ast_data o.class.name, @connection_id
|
148
|
+
end
|
149
|
+
|
150
|
+
if IGNORED_EXPRESSIONS.include?(o)
|
151
|
+
# Ignore
|
152
|
+
elsif context[:modifier]
|
153
|
+
QueryTracker.instance.add_modifier context[:modifier], o.to_s, @connection_id
|
154
|
+
end
|
155
|
+
end
|
156
|
+
alias :visit_Arel_Nodes_SqlLiteral :unsafe
|
157
|
+
alias :visit_String :unsafe
|
158
|
+
|
159
|
+
# We use a context (second argument of visit) to keep track of where we are in the AST.
|
160
|
+
# The following methods update the contexts based on the branch of the AST being visited
|
161
|
+
# and recursively visit the children nodes.
|
162
|
+
|
163
|
+
def visit_Arel_Nodes_SelectCore(o, context, _opts)
|
164
|
+
QueryTracker.instance.add_ast_data o.class, @connection_id
|
165
|
+
|
166
|
+
visit o.projections, modifier: :select
|
167
|
+
visit o.source, modifier: :from
|
168
|
+
visit o.wheres, modifier: :where
|
169
|
+
visit o.groups, modifier: :group
|
170
|
+
visit o.windows, context
|
171
|
+
visit o.having, modifier: :having
|
172
|
+
end
|
173
|
+
|
174
|
+
def visit_Arel_Nodes_SelectStatement(o, context, _opts)
|
175
|
+
QueryTracker.instance.add_ast_data o.class, @connection_id
|
176
|
+
|
177
|
+
visit o.cores, context
|
178
|
+
visit o.orders, modifier: :order
|
179
|
+
visit o.limit, modifier: :limit
|
180
|
+
visit o.lock, modifier: :lock
|
181
|
+
visit o.offset, modifier: :offset
|
182
|
+
end
|
183
|
+
|
184
|
+
def visit_Arel_Nodes_UpdateStatement(o, context, _opts)
|
185
|
+
QueryTracker.instance.add_ast_data o.class, @connection_id
|
186
|
+
|
187
|
+
visit o.relation, context
|
188
|
+
visit o.values, context
|
189
|
+
visit o.wheres, modifier: :where
|
190
|
+
visit o.orders, modifier: :order
|
191
|
+
visit o.limit, modifier: :limit
|
192
|
+
end
|
193
|
+
|
194
|
+
def visit_Arel_Nodes_DeleteStatement(o, context, _opts)
|
195
|
+
QueryTracker.instance.add_ast_data o.class, @connection_id
|
196
|
+
|
197
|
+
visit o.relation, context
|
198
|
+
visit o.wheres, modifier: :where
|
199
|
+
end
|
200
|
+
|
201
|
+
# All other methods bellow are for visiting each node and their children.
|
202
|
+
|
203
|
+
def unary(o, context, _opts)
|
204
|
+
QueryTracker.instance.add_ast_data o.class, @connection_id
|
205
|
+
|
206
|
+
visit o.expr, context
|
207
|
+
end
|
208
|
+
alias :visit_Arel_Nodes_Group :unary
|
209
|
+
alias :visit_Arel_Nodes_Grouping :unary
|
210
|
+
alias :visit_Arel_Nodes_Having :unary
|
211
|
+
alias :visit_Arel_Nodes_Limit :unary
|
212
|
+
alias :visit_Arel_Nodes_Not :unary
|
213
|
+
alias :visit_Arel_Nodes_Offset :unary
|
214
|
+
alias :visit_Arel_Nodes_On :unary
|
215
|
+
alias :visit_Arel_Nodes_Ordering :unary
|
216
|
+
alias :visit_Arel_Nodes_Ascending :unary
|
217
|
+
alias :visit_Arel_Nodes_Descending :unary
|
218
|
+
alias :visit_Arel_Nodes_Top :unary
|
219
|
+
alias :visit_Arel_Nodes_UnqualifiedColumn :unary
|
220
|
+
alias :visit_Arel_Nodes_Lock :unary
|
221
|
+
alias :visit_Arel_Nodes_Quoted :unary
|
222
|
+
|
223
|
+
def function(o, context, _opts)
|
224
|
+
QueryTracker.instance.add_ast_data o.class, @connection_id
|
225
|
+
|
226
|
+
visit o.expressions, context
|
227
|
+
end
|
228
|
+
alias :visit_Arel_Nodes_Avg :function
|
229
|
+
alias :visit_Arel_Nodes_Exists :function
|
230
|
+
alias :visit_Arel_Nodes_Max :function
|
231
|
+
alias :visit_Arel_Nodes_Min :function
|
232
|
+
alias :visit_Arel_Nodes_Sum :function
|
233
|
+
|
234
|
+
def visit_Arel_Nodes_NamedFunction(o, context, _opts)
|
235
|
+
QueryTracker.instance.add_ast_data o.class, @connection_id
|
236
|
+
|
237
|
+
visit o.expressions, context
|
238
|
+
end
|
239
|
+
|
240
|
+
def visit_Arel_Nodes_Count(o, context, _opts)
|
241
|
+
QueryTracker.instance.add_ast_data o.class, @connection_id
|
242
|
+
|
243
|
+
visit o.expressions, context
|
244
|
+
end
|
245
|
+
|
246
|
+
def nary(o, context, _opts)
|
247
|
+
QueryTracker.instance.add_ast_data o.class, @connection_id
|
248
|
+
|
249
|
+
o.children.each { |child| visit child, context }
|
250
|
+
end
|
251
|
+
alias :visit_Arel_Nodes_And :nary
|
252
|
+
|
253
|
+
def binary(o, context, _opts)
|
254
|
+
QueryTracker.instance.add_ast_data o.class, @connection_id
|
255
|
+
|
256
|
+
visit o.left, context
|
257
|
+
visit o.right, context
|
258
|
+
end
|
259
|
+
alias :visit_Arel_Nodes_As :binary
|
260
|
+
alias :visit_Arel_Nodes_Assignment :binary
|
261
|
+
alias :visit_Arel_Nodes_Between :binary
|
262
|
+
alias :visit_Arel_Nodes_DoesNotMatch :binary
|
263
|
+
alias :visit_Arel_Nodes_Equality :binary
|
264
|
+
alias :visit_Arel_Nodes_FullOuterJoin :binary
|
265
|
+
alias :visit_Arel_Nodes_GreaterThan :binary
|
266
|
+
alias :visit_Arel_Nodes_GreaterThanOrEqual :binary
|
267
|
+
alias :visit_Arel_Nodes_InfixOperation :binary
|
268
|
+
alias :visit_Arel_Nodes_JoinSource :binary
|
269
|
+
alias :visit_Arel_Nodes_InnerJoin :binary
|
270
|
+
alias :visit_Arel_Nodes_LessThan :binary
|
271
|
+
alias :visit_Arel_Nodes_LessThanOrEqual :binary
|
272
|
+
alias :visit_Arel_Nodes_Matches :binary
|
273
|
+
alias :visit_Arel_Nodes_NotEqual :binary
|
274
|
+
alias :visit_Arel_Nodes_NotRegexp :binary
|
275
|
+
alias :visit_Arel_Nodes_Or :binary
|
276
|
+
alias :visit_Arel_Nodes_OuterJoin :binary
|
277
|
+
alias :visit_Arel_Nodes_Regexp :binary
|
278
|
+
alias :visit_Arel_Nodes_RightOuterJoin :binary
|
279
|
+
alias :visit_Arel_Nodes_TableAlias :binary
|
280
|
+
alias :visit_Arel_Nodes_Values :binary
|
281
|
+
alias :visit_Arel_Nodes_Union :binary
|
282
|
+
|
283
|
+
# Special case the IN clause node. We don't want to add info about AST
|
284
|
+
# nodes in the right side of an IN clause if they are terminal or an
|
285
|
+
# array of terminal nodes.
|
286
|
+
def visit_Arel_Nodes_In(o, context, _opts)
|
287
|
+
QueryTracker.instance.add_ast_data o.class, @connection_id
|
288
|
+
|
289
|
+
visit o.left, context
|
290
|
+
visit o.right, context, IN_children: true
|
291
|
+
end
|
292
|
+
alias :visit_Arel_Nodes_NotIn :visit_Arel_Nodes_In
|
293
|
+
|
294
|
+
def visit_Arel_Nodes_StringJoin(o, context, _opts)
|
295
|
+
QueryTracker.instance.add_ast_data o.class, @connection_id
|
296
|
+
|
297
|
+
visit o.left, context
|
298
|
+
end
|
299
|
+
|
300
|
+
def visit_Arel_Attribute(o, context, _opts)
|
301
|
+
QueryTracker.instance.add_ast_data o.class, @connection_id
|
302
|
+
|
303
|
+
visit o.relation, context
|
304
|
+
end
|
305
|
+
alias :visit_Arel_Attributes_Integer :visit_Arel_Attribute
|
306
|
+
alias :visit_Arel_Attributes_Float :visit_Arel_Attribute
|
307
|
+
alias :visit_Arel_Attributes_String :visit_Arel_Attribute
|
308
|
+
alias :visit_Arel_Attributes_Time :visit_Arel_Attribute
|
309
|
+
alias :visit_Arel_Attributes_Boolean :visit_Arel_Attribute
|
310
|
+
alias :visit_Arel_Attributes_Attribute :visit_Arel_Attribute
|
311
|
+
alias :visit_Arel_Attributes_Decimal :visit_Arel_Attribute
|
312
|
+
|
313
|
+
def visit_Arel_Table(o, _context, _opts)
|
314
|
+
QueryTracker.instance.add_ast_data o.class, @connection_id
|
315
|
+
end
|
316
|
+
|
317
|
+
def terminal(_o, _context, _opts)
|
318
|
+
end
|
319
|
+
alias :visit_Arel_Nodes_Node :terminal
|
320
|
+
alias :visit_ActiveSupport_Multibyte_Chars :terminal
|
321
|
+
alias :visit_ActiveSupport_StringInquirer :terminal
|
322
|
+
alias :visit_Symbol :terminal
|
323
|
+
alias :visit_Arel_Nodes_Window :terminal
|
324
|
+
alias :visit_Arel_Nodes_True :terminal
|
325
|
+
alias :visit_Arel_Nodes_False :terminal
|
326
|
+
alias :visit_BigDecimal :terminal
|
327
|
+
alias :visit_Bignum :terminal
|
328
|
+
alias :visit_Class :terminal
|
329
|
+
alias :visit_Date :terminal
|
330
|
+
alias :visit_DateTime :terminal
|
331
|
+
alias :visit_FalseClass :terminal
|
332
|
+
alias :visit_Fixnum :terminal
|
333
|
+
alias :visit_Float :terminal
|
334
|
+
alias :visit_Arel_Nodes_BindParam :terminal
|
335
|
+
alias :visit_NilClass :terminal
|
336
|
+
alias :visit_Time :terminal
|
337
|
+
alias :visit_TrueClass :terminal
|
338
|
+
alias :visit_Object :terminal
|
339
|
+
|
340
|
+
def visit_Arel_Nodes_InsertStatement(o, context, _opts)
|
341
|
+
QueryTracker.instance.add_ast_data o.class, @connection_id
|
342
|
+
|
343
|
+
visit o.relation, context
|
344
|
+
visit o.columns, context
|
345
|
+
visit o.values, context
|
346
|
+
end
|
347
|
+
|
348
|
+
def visit_Array(o, context, opts)
|
349
|
+
unless opts[:IN_children]
|
350
|
+
QueryTracker.instance.add_ast_data o.class, @connection_id
|
351
|
+
end
|
352
|
+
|
353
|
+
o.each { |i| visit i, context }
|
354
|
+
end
|
355
|
+
alias :visit_Set :visit_Array
|
356
|
+
|
357
|
+
def visit_Hash(o, context, _opts)
|
358
|
+
QueryTracker.instance.add_ast_data o.class, @connection_id
|
359
|
+
|
360
|
+
o.each { |k,v| visit(k, context); visit(v, context) }
|
361
|
+
end
|
362
|
+
end
|
363
|
+
|
364
|
+
class QueryTracker
|
365
|
+
include Singleton
|
366
|
+
|
367
|
+
def initialize
|
368
|
+
# The data in these hashes represent relations and connections whose
|
369
|
+
# lifecycle cannot be easily inferred. A relation could be kept around
|
370
|
+
# across multiple HTTP requests, for example. We defined finalizers to
|
371
|
+
# clean up data in the hashes when the objects they are linked to are
|
372
|
+
# released by the Ruby runtime.
|
373
|
+
#
|
374
|
+
# Note: Relations have an associated connection and are not accessed by
|
375
|
+
# other connections. Connections are associated with a thread and are not
|
376
|
+
# accessed by other threads at the same time. Thus, there is no need for
|
377
|
+
# thread safety in any of the logic in this class.
|
378
|
+
|
379
|
+
# Data about a relation. The data inside is stored at different times and
|
380
|
+
# must be reset properly:
|
381
|
+
#
|
382
|
+
# * params and relation_data: Added when ActiveRecord::Relation API calls
|
383
|
+
# are made, like #where. Should never be reset for a given relation.
|
384
|
+
# * ast_data and modifiers: Added when a relation is converted
|
385
|
+
# into a SQL query statement. Should be reset after every query
|
386
|
+
# execution.
|
387
|
+
@relation_data = Hash.new do |relation_data, relation_id|
|
388
|
+
# This should never happen, but if it does it's a sign of an impending
|
389
|
+
# memory leak. Log it, but just let it happen as it would be hard to
|
390
|
+
# handle elsewhere if we did not set up the data.
|
391
|
+
unless ObjectSpace._id2ref(relation_id).is_a? ActiveRecord::Relation
|
392
|
+
name = if ObjectSpace._id2ref(relation_id).is_a? Class
|
393
|
+
"#{ObjectSpace._id2ref(relation_id).name} Class"
|
394
|
+
else
|
395
|
+
"#{ObjectSpace._id2ref(relation_id).class.name} Instance"
|
396
|
+
end # rubocop:disable Lint/EndAlignment
|
397
|
+
Immunio.logger.warn {"Creating relation data for non-relation: #{name}"}
|
398
|
+
Immunio.logger.debug {"Call stack:\n#{caller.join "\n"}"}
|
399
|
+
end
|
400
|
+
|
401
|
+
# NOTE: If you hold a reference to the relation here, like say:
|
402
|
+
#
|
403
|
+
# relation = ObjectSpace._id2ref(relation_id)
|
404
|
+
#
|
405
|
+
# the scope for the block will hold the relation and it will never be
|
406
|
+
# released.
|
407
|
+
ObjectSpace.define_finalizer ObjectSpace._id2ref(relation_id),
|
408
|
+
self.class.finalize_relation(relation_id)
|
409
|
+
|
410
|
+
relation_data[relation_id] = {
|
411
|
+
params: {},
|
412
|
+
relation_data: [],
|
413
|
+
ast_data: [],
|
414
|
+
modifiers: Hash.new do |modifiers, type|
|
415
|
+
modifiers[type] = []
|
416
|
+
end
|
417
|
+
}
|
418
|
+
end
|
419
|
+
|
420
|
+
# Stacks of relations for each connection. Used to find the appropriate
|
421
|
+
# relation for a connection when a query is executed. A stack is used
|
422
|
+
# because some relation methods create new relations and call other
|
423
|
+
# relation methods on the new relations.
|
424
|
+
@relations = Hash.new do |relations, connection_id|
|
425
|
+
connection = ObjectSpace._id2ref(connection_id)
|
426
|
+
ObjectSpace.define_finalizer(connection, self.class.finalize_connection(connection_id))
|
427
|
+
|
428
|
+
relations[connection_id] = []
|
429
|
+
end
|
430
|
+
|
431
|
+
# Last spawned relations for connections. Used for a hack to propagate
|
432
|
+
# params to the right relation in Rails 3.
|
433
|
+
@last_spawned_relations = {}
|
434
|
+
end
|
435
|
+
|
436
|
+
# Delete a relation record when the relation object is released.
|
437
|
+
def self.finalize_relation(relation_id)
|
438
|
+
proc do
|
439
|
+
relation_data = instance.instance_variable_get(:@relation_data)
|
440
|
+
|
441
|
+
# Check if key exists, delete will call the default value block if not
|
442
|
+
relation_data.delete(relation_id) if relation_data.has_key? relation_id
|
443
|
+
end
|
444
|
+
end
|
445
|
+
|
446
|
+
# Delete a connection record when the connection object is released.
|
447
|
+
def self.finalize_connection(connection_id)
|
448
|
+
proc do
|
449
|
+
relations = instance.instance_variable_get(:@relations)
|
450
|
+
|
451
|
+
# Check if key exists, delete will call the default value block if not
|
452
|
+
relations.delete(connection_id) if relations.has_key? connection_id
|
453
|
+
|
454
|
+
instance.instance_variable_get(:@last_spawned_relations).delete connection_id
|
455
|
+
end
|
456
|
+
end
|
457
|
+
|
458
|
+
# Push a relation onto the stack for its connection
|
459
|
+
def push_relation(relation)
|
460
|
+
@relations[relation.connection.object_id] << relation.object_id
|
461
|
+
end
|
462
|
+
|
463
|
+
# Pop a relation off the stack for its connection
|
464
|
+
def pop_relation(relation)
|
465
|
+
popped = @relations[relation.connection.object_id].pop
|
466
|
+
unless popped == relation.object_id
|
467
|
+
Immunio.logger.warn {"Popped wrong relation, expected: #{relation}, popped: #{popped}"}
|
468
|
+
Immunio.logger.debug {"Call stack:\n#{caller.join "\n"}"}
|
469
|
+
end
|
470
|
+
end
|
471
|
+
|
472
|
+
# Called when a relation is cloned. The data for the new relation must also
|
473
|
+
# be copied from the old relation.
|
474
|
+
def spawn_relation(orig, new)
|
475
|
+
orig_id = orig.object_id
|
476
|
+
new_id = new.object_id
|
477
|
+
|
478
|
+
# If we weren't tracking the original relation, don't bother setting up
|
479
|
+
# the new relation yet.
|
480
|
+
if @relation_data.has_key? orig_id
|
481
|
+
# ast_data and modifiers should be empty, but we must clone modifiers to
|
482
|
+
# get the initializer.
|
483
|
+
@relation_data[new_id] = {
|
484
|
+
params: @relation_data[orig_id][:params].clone,
|
485
|
+
relation_data: @relation_data[orig_id][:relation_data].clone,
|
486
|
+
ast_data: @relation_data[orig_id][:ast_data] = [],
|
487
|
+
modifiers: @relation_data[orig_id][:modifiers].clone
|
488
|
+
}
|
489
|
+
|
490
|
+
# The default block for the @relation_data hash isn't called when
|
491
|
+
# assigning a value to a new key. We must set up the finalizer manually.
|
492
|
+
ObjectSpace.define_finalizer(new, self.class.finalize_relation(new_id))
|
493
|
+
end
|
494
|
+
|
495
|
+
# Save the last spawned relation for a hack for storing params from #where
|
496
|
+
# and #having.
|
497
|
+
@last_spawned_relations[new.connection.object_id] = new_id
|
498
|
+
end
|
499
|
+
|
500
|
+
# Retrieve the last spawned relation for the connection. This is an ugly
|
501
|
+
# hack for a poor implementation of the #where and #having methods in Rails
|
502
|
+
# 3.
|
503
|
+
def last_spawned_relation(connection)
|
504
|
+
ObjectSpace._id2ref @last_spawned_relations[connection.object_id]
|
505
|
+
end
|
506
|
+
|
507
|
+
# Called when two relations are merged. The data for the other relation must
|
508
|
+
# be copied into the current relation. AST data and modifiers should be
|
509
|
+
# empty and don't need to be copied.
|
510
|
+
def merge_relations(relation, other)
|
511
|
+
params = @relation_data[relation.object_id][:params]
|
512
|
+
other_params = @relation_data[other.object_id][:params]
|
513
|
+
|
514
|
+
other_params.each_pair do |name, value|
|
515
|
+
# Update numeric ID for current relation if name is an integer.
|
516
|
+
name = params.size.to_s if name.to_i.to_s == name
|
517
|
+
|
518
|
+
params[name] = value
|
519
|
+
end
|
520
|
+
|
521
|
+
other_data = @relation_data[other.object_id][:relation_data]
|
522
|
+
@relation_data[relation.object_id][:relation_data] += other_data
|
523
|
+
end
|
524
|
+
|
525
|
+
# Add relation API context data to the relation.
|
526
|
+
def add_relation_data(relation, data)
|
527
|
+
@relation_data[relation.object_id][:relation_data] << data
|
528
|
+
end
|
529
|
+
|
530
|
+
# Add a parameter to the current relation for the connection. If the
|
531
|
+
# relation is copied or merged into another relation, the param will also
|
532
|
+
# be copied.
|
533
|
+
def add_param(name, value, connection_id)
|
534
|
+
relation_id = @relations[connection_id].last
|
535
|
+
|
536
|
+
# This can occur if the query statement isn't generated by the app but by
|
537
|
+
# ActiveRecord itself.
|
538
|
+
return unless relation_id
|
539
|
+
|
540
|
+
params = @relation_data[relation_id][:params]
|
541
|
+
|
542
|
+
# If no name given, use index.
|
543
|
+
if name.nil?
|
544
|
+
name = params.size.to_s
|
545
|
+
end
|
546
|
+
|
547
|
+
Immunio.logger.debug "Adding ActiveRecord SQL param to relation #{relation_id} (name: #{name}, value: #{value})"
|
548
|
+
|
549
|
+
params[name] = value
|
550
|
+
end
|
551
|
+
|
552
|
+
# Add a modifier to the current relation for the connection. This only
|
553
|
+
# occurs during conversion of a relation to SQL statement, so modifiers are
|
554
|
+
# never copied from one relation to another.
|
555
|
+
def add_modifier(type, value, connection_id)
|
556
|
+
relation_id = @relations[connection_id].last
|
557
|
+
|
558
|
+
# This can occur if the query statement isn't generated by the app but by
|
559
|
+
# ActiveRecord itself.
|
560
|
+
return unless relation_id
|
561
|
+
|
562
|
+
@relation_data[relation_id][:modifiers][type] << value
|
563
|
+
end
|
564
|
+
|
565
|
+
# Add data about an Arel AST node to the context data for the connection.
|
566
|
+
# This only occurs during conversion of a relation to SQL statement, so AST
|
567
|
+
# context data is never copied from one relation to another.
|
568
|
+
def add_ast_data(ast_node_name, connection_id)
|
569
|
+
relation_id = @relations[connection_id].last
|
570
|
+
|
571
|
+
# This can occur if the query statement was cached and there's no relation
|
572
|
+
# associated with the connection. That's ok here, though, because there's
|
573
|
+
# such a limited number of cacheable statement structures that we don't
|
574
|
+
# need AST info to differentiate between queries with the same stack
|
575
|
+
# trace.
|
576
|
+
return unless relation_id
|
577
|
+
|
578
|
+
@relation_data[relation_id][:ast_data] << "Arel AST visited node: #{ast_node_name}"
|
579
|
+
end
|
580
|
+
|
581
|
+
# Evaluate a SQL call. This occurs after Arel AST conversion of a relation
|
582
|
+
# to a statement.
|
583
|
+
def call(payload)
|
584
|
+
Request.time "plugin", "#{Module.nesting[0]}::#{__method__}" do
|
585
|
+
Immunio.logger.debug "New ActiveRecord SQL query: #{payload}"
|
586
|
+
|
587
|
+
connection_id = payload[:connection_id]
|
588
|
+
|
589
|
+
relation_id = @relations[connection_id].last
|
590
|
+
|
591
|
+
if should_ignore? payload[:sql]
|
592
|
+
Immunio.logger.debug "Ignoring query as it was generated by ActiveRecord itself (#{payload[:sql]})"
|
593
|
+
return
|
594
|
+
end
|
595
|
+
|
596
|
+
if relation_id
|
597
|
+
# Note: If a relation is released between when it is converted to a
|
598
|
+
# SQL statement and now, we would lose the data and additionally leak
|
599
|
+
# an empty entry in the @relation_data hash. I don't believe this is
|
600
|
+
# possible due to how we wrap things, but there's no explicit
|
601
|
+
# guarantee.
|
602
|
+
relation_data = @relation_data[relation_id]
|
603
|
+
params = relation_data[:params].clone
|
604
|
+
context_data = (relation_data[:relation_data] + relation_data[:ast_data]).join "\n"
|
605
|
+
|
606
|
+
# modifiers must be cloned because it will be cleared when the
|
607
|
+
# relation is reset.
|
608
|
+
modifiers = relation_data[:modifiers].clone
|
609
|
+
else
|
610
|
+
params = {}
|
611
|
+
context_data = nil
|
612
|
+
modifiers = {}
|
613
|
+
end
|
614
|
+
|
615
|
+
# Merge bound values
|
616
|
+
question_marks = 0
|
617
|
+
payload[:binds].each do |(column, value)|
|
618
|
+
if column.nil?
|
619
|
+
params["?:#{question_marks}"] = value.to_s
|
620
|
+
question_marks = question_marks + 1
|
621
|
+
else
|
622
|
+
# When using the activerecord-sqlserver-adapter gem, the "column" is
|
623
|
+
# the actual param name.
|
624
|
+
name = column.respond_to?(:name) ? column.name : column.to_s
|
625
|
+
params[name] = value.to_s
|
626
|
+
end
|
627
|
+
end
|
628
|
+
|
629
|
+
strict_context, loose_context, stack = Immunio::Context.context context_data
|
630
|
+
|
631
|
+
# Send in additional_context_data for debugging purposes
|
632
|
+
Immunio.run_hook! "active_record", "sql_execute", sql: payload[:sql],
|
633
|
+
connection_uuid: connection_id.to_s,
|
634
|
+
params: params,
|
635
|
+
modifiers: modifiers,
|
636
|
+
context_key: strict_context,
|
637
|
+
loose_context_key: loose_context,
|
638
|
+
stack: stack,
|
639
|
+
additional_context_data: context_data
|
640
|
+
|
641
|
+
reset relation_id
|
642
|
+
end
|
643
|
+
end
|
644
|
+
|
645
|
+
# Reset per-execution data for a relation.
|
646
|
+
def reset(relation_id)
|
647
|
+
return unless relation_id
|
648
|
+
|
649
|
+
[:ast_data, :modifiers].each do |type|
|
650
|
+
@relation_data[relation_id][type].clear
|
651
|
+
end
|
652
|
+
end
|
653
|
+
|
654
|
+
private
|
655
|
+
IGNORE_START_WITH = ['PRAGMA', 'SHOW', 'SAVEPOINT', 'RELEASE', 'ROLLBACK']
|
656
|
+
IGNORE_SQL = /^(begin|commit|rollback)(?: transaction)$/i
|
657
|
+
def should_ignore?(sql)
|
658
|
+
# Ignore queries generated by ActiveRecord.
|
659
|
+
return sql.start_with?(*IGNORE_START_WITH) || IGNORE_SQL =~ sql
|
660
|
+
end
|
661
|
+
end
|
662
|
+
|
663
|
+
# Hook into the SQL query execution methods of Rails.
|
664
|
+
# Since all executed queries inside Rails are logged, we hook into the `log` method to catch them all.
|
665
|
+
module QueryExecutionHooks
|
666
|
+
extend ActiveSupport::Concern
|
667
|
+
|
668
|
+
included do
|
669
|
+
alias_method_chain :log, :immunio if method_defined? :log
|
670
|
+
end
|
671
|
+
|
672
|
+
def log_with_immunio(sql, name = "SQL", binds = [], *args)
|
673
|
+
QueryTracker.instance.call sql: sql,
|
674
|
+
connection_id: object_id,
|
675
|
+
binds: binds
|
676
|
+
|
677
|
+
# Log and execute the query
|
678
|
+
log_without_immunio(sql, name, binds, *args) { yield }
|
679
|
+
end
|
680
|
+
end
|
681
|
+
end
|
682
|
+
|
683
|
+
# Hook into quoting methods at the highest level possible in the ancestors chain.
|
684
|
+
# In case the quote methods were overridden in a child class.
|
685
|
+
module ActiveRecord::ConnectionAdapters
|
686
|
+
if defined? Mysql2Adapter
|
687
|
+
Mysql2Adapter.send :include, Immunio::QuotingHooks
|
688
|
+
elsif defined? MysqlAdapter
|
689
|
+
MysqlAdapter.send :include, Immunio::QuotingHooks
|
690
|
+
end
|
691
|
+
if defined? PostgreSQLAdapter
|
692
|
+
PostgreSQLAdapter.send :include, Immunio::QuotingHooks
|
693
|
+
end
|
694
|
+
if defined? SQLite3Adapter
|
695
|
+
SQLite3Adapter.send :include, Immunio::QuotingHooks
|
696
|
+
elsif defined? SQLiteAdapter
|
697
|
+
SQLiteAdapter.send :include, Immunio::QuotingHooks
|
698
|
+
end
|
699
|
+
end
|
700
|
+
|
701
|
+
module ActiveRecord::Sanitization
|
702
|
+
ClassMethods.send :include, Immunio::SanitizeHooks
|
703
|
+
end
|
704
|
+
|
705
|
+
Arel::Visitors::ToSql.send :include, Immunio::ArelToSqlHooks
|
706
|
+
|
707
|
+
ActiveRecord::ConnectionAdapters::AbstractAdapter.send :include, Immunio::QueryExecutionHooks
|