immunio 0.15.2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (157) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE +234 -0
  3. data/README.md +147 -0
  4. data/bin/immunio +5 -0
  5. data/lib/immunio.rb +29 -0
  6. data/lib/immunio/agent.rb +260 -0
  7. data/lib/immunio/authentication.rb +96 -0
  8. data/lib/immunio/blocked_app.rb +38 -0
  9. data/lib/immunio/channel.rb +432 -0
  10. data/lib/immunio/cli.rb +39 -0
  11. data/lib/immunio/context.rb +114 -0
  12. data/lib/immunio/errors.rb +43 -0
  13. data/lib/immunio/immunio_ca.crt +45 -0
  14. data/lib/immunio/logger.rb +87 -0
  15. data/lib/immunio/plugins/action_dispatch.rb +45 -0
  16. data/lib/immunio/plugins/action_view.rb +431 -0
  17. data/lib/immunio/plugins/active_record.rb +707 -0
  18. data/lib/immunio/plugins/active_record_relation.rb +370 -0
  19. data/lib/immunio/plugins/authlogic.rb +80 -0
  20. data/lib/immunio/plugins/csrf.rb +24 -0
  21. data/lib/immunio/plugins/devise.rb +40 -0
  22. data/lib/immunio/plugins/environment_reporter.rb +69 -0
  23. data/lib/immunio/plugins/eval.rb +51 -0
  24. data/lib/immunio/plugins/exception_handler.rb +55 -0
  25. data/lib/immunio/plugins/gems_tracker.rb +5 -0
  26. data/lib/immunio/plugins/haml.rb +36 -0
  27. data/lib/immunio/plugins/http_finisher.rb +50 -0
  28. data/lib/immunio/plugins/http_tracker.rb +203 -0
  29. data/lib/immunio/plugins/io.rb +96 -0
  30. data/lib/immunio/plugins/redirect.rb +42 -0
  31. data/lib/immunio/plugins/warden.rb +66 -0
  32. data/lib/immunio/processor.rb +234 -0
  33. data/lib/immunio/rails.rb +26 -0
  34. data/lib/immunio/request.rb +139 -0
  35. data/lib/immunio/rufus_lua_ext/ref.rb +27 -0
  36. data/lib/immunio/rufus_lua_ext/state.rb +157 -0
  37. data/lib/immunio/rufus_lua_ext/table.rb +137 -0
  38. data/lib/immunio/rufus_lua_ext/utils.rb +13 -0
  39. data/lib/immunio/version.rb +5 -0
  40. data/lib/immunio/vm.rb +291 -0
  41. data/lua-hooks/ext/all.c +78 -0
  42. data/lua-hooks/ext/bitop/README +22 -0
  43. data/lua-hooks/ext/bitop/bit.c +189 -0
  44. data/lua-hooks/ext/extconf.rb +38 -0
  45. data/lua-hooks/ext/libinjection/COPYING +37 -0
  46. data/lua-hooks/ext/libinjection/libinjection.h +65 -0
  47. data/lua-hooks/ext/libinjection/libinjection_html5.c +847 -0
  48. data/lua-hooks/ext/libinjection/libinjection_html5.h +54 -0
  49. data/lua-hooks/ext/libinjection/libinjection_sqli.c +2301 -0
  50. data/lua-hooks/ext/libinjection/libinjection_sqli.h +295 -0
  51. data/lua-hooks/ext/libinjection/libinjection_sqli_data.h +9349 -0
  52. data/lua-hooks/ext/libinjection/libinjection_xss.c +531 -0
  53. data/lua-hooks/ext/libinjection/libinjection_xss.h +21 -0
  54. data/lua-hooks/ext/libinjection/lualib.c +109 -0
  55. data/lua-hooks/ext/lpeg/HISTORY +90 -0
  56. data/lua-hooks/ext/lpeg/lpcap.c +537 -0
  57. data/lua-hooks/ext/lpeg/lpcap.h +43 -0
  58. data/lua-hooks/ext/lpeg/lpcode.c +986 -0
  59. data/lua-hooks/ext/lpeg/lpcode.h +34 -0
  60. data/lua-hooks/ext/lpeg/lpeg-128.gif +0 -0
  61. data/lua-hooks/ext/lpeg/lpeg.html +1429 -0
  62. data/lua-hooks/ext/lpeg/lpprint.c +244 -0
  63. data/lua-hooks/ext/lpeg/lpprint.h +35 -0
  64. data/lua-hooks/ext/lpeg/lptree.c +1238 -0
  65. data/lua-hooks/ext/lpeg/lptree.h +77 -0
  66. data/lua-hooks/ext/lpeg/lptypes.h +149 -0
  67. data/lua-hooks/ext/lpeg/lpvm.c +355 -0
  68. data/lua-hooks/ext/lpeg/lpvm.h +58 -0
  69. data/lua-hooks/ext/lpeg/makefile +55 -0
  70. data/lua-hooks/ext/lpeg/re.html +498 -0
  71. data/lua-hooks/ext/lpeg/test.lua +1409 -0
  72. data/lua-hooks/ext/lua-cmsgpack/CMakeLists.txt +45 -0
  73. data/lua-hooks/ext/lua-cmsgpack/README.md +115 -0
  74. data/lua-hooks/ext/lua-cmsgpack/lua_cmsgpack.c +957 -0
  75. data/lua-hooks/ext/lua-cmsgpack/test.lua +570 -0
  76. data/lua-hooks/ext/lua-snapshot/LICENSE +7 -0
  77. data/lua-hooks/ext/lua-snapshot/Makefile +12 -0
  78. data/lua-hooks/ext/lua-snapshot/README.md +18 -0
  79. data/lua-hooks/ext/lua-snapshot/dump.lua +15 -0
  80. data/lua-hooks/ext/lua-snapshot/snapshot.c +455 -0
  81. data/lua-hooks/ext/lua/COPYRIGHT +34 -0
  82. data/lua-hooks/ext/lua/lapi.c +1087 -0
  83. data/lua-hooks/ext/lua/lapi.h +16 -0
  84. data/lua-hooks/ext/lua/lauxlib.c +652 -0
  85. data/lua-hooks/ext/lua/lauxlib.h +174 -0
  86. data/lua-hooks/ext/lua/lbaselib.c +659 -0
  87. data/lua-hooks/ext/lua/lcode.c +831 -0
  88. data/lua-hooks/ext/lua/lcode.h +76 -0
  89. data/lua-hooks/ext/lua/ldblib.c +398 -0
  90. data/lua-hooks/ext/lua/ldebug.c +638 -0
  91. data/lua-hooks/ext/lua/ldebug.h +33 -0
  92. data/lua-hooks/ext/lua/ldo.c +519 -0
  93. data/lua-hooks/ext/lua/ldo.h +57 -0
  94. data/lua-hooks/ext/lua/ldump.c +164 -0
  95. data/lua-hooks/ext/lua/lfunc.c +174 -0
  96. data/lua-hooks/ext/lua/lfunc.h +34 -0
  97. data/lua-hooks/ext/lua/lgc.c +710 -0
  98. data/lua-hooks/ext/lua/lgc.h +110 -0
  99. data/lua-hooks/ext/lua/linit.c +38 -0
  100. data/lua-hooks/ext/lua/liolib.c +556 -0
  101. data/lua-hooks/ext/lua/llex.c +463 -0
  102. data/lua-hooks/ext/lua/llex.h +81 -0
  103. data/lua-hooks/ext/lua/llimits.h +128 -0
  104. data/lua-hooks/ext/lua/lmathlib.c +263 -0
  105. data/lua-hooks/ext/lua/lmem.c +86 -0
  106. data/lua-hooks/ext/lua/lmem.h +49 -0
  107. data/lua-hooks/ext/lua/loadlib.c +705 -0
  108. data/lua-hooks/ext/lua/loadlib_rel.c +760 -0
  109. data/lua-hooks/ext/lua/lobject.c +214 -0
  110. data/lua-hooks/ext/lua/lobject.h +381 -0
  111. data/lua-hooks/ext/lua/lopcodes.c +102 -0
  112. data/lua-hooks/ext/lua/lopcodes.h +268 -0
  113. data/lua-hooks/ext/lua/loslib.c +243 -0
  114. data/lua-hooks/ext/lua/lparser.c +1339 -0
  115. data/lua-hooks/ext/lua/lparser.h +82 -0
  116. data/lua-hooks/ext/lua/lstate.c +214 -0
  117. data/lua-hooks/ext/lua/lstate.h +169 -0
  118. data/lua-hooks/ext/lua/lstring.c +111 -0
  119. data/lua-hooks/ext/lua/lstring.h +31 -0
  120. data/lua-hooks/ext/lua/lstrlib.c +871 -0
  121. data/lua-hooks/ext/lua/ltable.c +588 -0
  122. data/lua-hooks/ext/lua/ltable.h +40 -0
  123. data/lua-hooks/ext/lua/ltablib.c +287 -0
  124. data/lua-hooks/ext/lua/ltm.c +75 -0
  125. data/lua-hooks/ext/lua/ltm.h +54 -0
  126. data/lua-hooks/ext/lua/lua.c +392 -0
  127. data/lua-hooks/ext/lua/lua.def +131 -0
  128. data/lua-hooks/ext/lua/lua.h +388 -0
  129. data/lua-hooks/ext/lua/lua.rc +28 -0
  130. data/lua-hooks/ext/lua/lua_dll.rc +26 -0
  131. data/lua-hooks/ext/lua/luac.c +200 -0
  132. data/lua-hooks/ext/lua/luac.rc +1 -0
  133. data/lua-hooks/ext/lua/luaconf.h +763 -0
  134. data/lua-hooks/ext/lua/luaconf.h.in +724 -0
  135. data/lua-hooks/ext/lua/luaconf.h.orig +763 -0
  136. data/lua-hooks/ext/lua/lualib.h +53 -0
  137. data/lua-hooks/ext/lua/lundump.c +227 -0
  138. data/lua-hooks/ext/lua/lundump.h +36 -0
  139. data/lua-hooks/ext/lua/lvm.c +767 -0
  140. data/lua-hooks/ext/lua/lvm.h +36 -0
  141. data/lua-hooks/ext/lua/lzio.c +82 -0
  142. data/lua-hooks/ext/lua/lzio.h +67 -0
  143. data/lua-hooks/ext/lua/print.c +227 -0
  144. data/lua-hooks/ext/luautf8/README.md +152 -0
  145. data/lua-hooks/ext/luautf8/lutf8lib.c +1274 -0
  146. data/lua-hooks/ext/luautf8/unidata.h +3064 -0
  147. data/lua-hooks/lib/boot.lua +254 -0
  148. data/lua-hooks/lib/encode.lua +4 -0
  149. data/lua-hooks/lib/lexers/LICENSE +21 -0
  150. data/lua-hooks/lib/lexers/bash.lua +134 -0
  151. data/lua-hooks/lib/lexers/bash_dqstr.lua +62 -0
  152. data/lua-hooks/lib/lexers/css.lua +216 -0
  153. data/lua-hooks/lib/lexers/html.lua +106 -0
  154. data/lua-hooks/lib/lexers/javascript.lua +68 -0
  155. data/lua-hooks/lib/lexers/lexer.lua +1575 -0
  156. data/lua-hooks/lib/lexers/markers.lua +33 -0
  157. 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