Skip to content

Commit 6065e29

Browse files
committed
Fix performance issues related to a x-product between ActiveRecordModelInstantiation and MethodCall
1 parent 43a4968 commit 6065e29

File tree

4 files changed

+77
-56
lines changed

4 files changed

+77
-56
lines changed

ql/lib/codeql/ruby/Concepts.qll

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -546,8 +546,9 @@ module XmlParserCall {
546546
* extend `OrmInstantiation::Range` instead.
547547
*/
548548
class OrmInstantiation extends DataFlow::Node instanceof OrmInstantiation::Range {
549-
/** Holds if `call` may return a field of this ORM object. */
550-
predicate methodCallMayAccessField(MethodCall call) { super.methodCallMayAccessField(call) }
549+
/** Holds if a call to `methodName` on this instance may return a field of this ORM object. */
550+
bindingset[methodName]
551+
predicate methodCallMayAccessField(string methodName) { super.methodCallMayAccessField(methodName) }
551552
}
552553

553554
/** Provides a class for modeling new ORM object instantiation APIs. */
@@ -559,7 +560,8 @@ module OrmInstantiation {
559560
* extend `OrmInstantiation` instead.
560561
*/
561562
abstract class Range extends DataFlow::Node {
562-
/** Holds if `call` may return a field of this ORM object. */
563-
abstract predicate methodCallMayAccessField(MethodCall call);
563+
/** Holds if a call to `methodName` on this instance may return a field of this ORM object. */
564+
bindingset[methodName]
565+
abstract predicate methodCallMayAccessField(string methodName);
564566
}
565567
}

ql/lib/codeql/ruby/frameworks/ActiveRecord.qll

Lines changed: 36 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,25 @@ private class ApplicationRecordAccess extends ConstantReadAccess {
2121
ApplicationRecordAccess() { this.getName() = "ApplicationRecord" }
2222
}
2323

24+
/// See https://api.rubyonrails.org/classes/ActiveRecord/Persistence.html
25+
private string activeRecordPersistenceInstanceMethodName() {
26+
result =
27+
[
28+
"becomes", "becomes!", "decrement", "decrement!", "delete", "delete!", "destroy", "destroy!",
29+
"destroyed?", "increment", "increment!", "new_record?", "persisted?",
30+
"previously_new_record?", "reload", "save", "save!", "toggle", "toggle!", "touch", "update",
31+
"update!", "update_attribute", "update_column", "update_columns"
32+
]
33+
}
34+
35+
// Methods with these names are defined for all active record model instances,
36+
// so they are unlikely to refer to a database field.
37+
private predicate isBuiltInMethodForActiveRecordModelInstance(string methodName) {
38+
methodName = activeRecordPersistenceInstanceMethodName() or
39+
methodName = basicObjectInstanceMethodName() or
40+
methodName = objectInstanceMethodName()
41+
}
42+
2443
/**
2544
* A `ClassDeclaration` for a class that extends `ActiveRecord::Base`. For example,
2645
*
@@ -52,11 +71,14 @@ class ActiveRecordModelClass extends ClassDeclaration {
5271
// There is a value that can be returned by this method which may include field data
5372
exists(DataFlow::Node returned, ActiveRecordInstanceMethodCall cNode, MethodCall c |
5473
exprNodeReturnedFrom(returned, result) and cNode.flowsTo(returned) and c = cNode.asExpr().getExpr() |
55-
// The referenced method is not built-in, and
56-
not isCallToBuiltInMethod(c) and (
57-
// There is no matching method definition in the class, or
74+
// The referenced method is not built-in, and...
75+
not isBuiltInMethodForActiveRecordModelInstance(c.getMethodName()) and (
76+
// TODO: this would be more accurate if we also checked methods defined in
77+
// super classes and mixins
78+
79+
// ...There is no matching method definition in the class, or...
5880
not exists(cNode.getInstance().getClass().getMethod(c.getMethodName())) or
59-
// The called method can access a field
81+
// ...the called method can access a field
6082
c.getATarget() = cNode.getInstance().getClass().methodMayAccessField()
6183
)
6284
)
@@ -202,18 +224,17 @@ private string constantQualifiedName(ConstantWriteAccess w) {
202224
abstract class ActiveRecordModelInstantiation extends OrmInstantiation::Range, DataFlow::LocalSourceNode {
203225
abstract ActiveRecordModelClass getClass();
204226

205-
override predicate methodCallMayAccessField(MethodCall call) {
206-
// The method is not a built-in
207-
not isCallToBuiltInMethod(call) and (
208-
// There is no matching method definition in the class, or
209-
not exists(this.getClass().getMethod(call.getMethodName())) or
210-
// The called method can access a field
227+
bindingset[methodName]
228+
override predicate methodCallMayAccessField(string methodName) {
229+
// The method is not a built-in, and...
230+
not isBuiltInMethodForActiveRecordModelInstance(methodName) and (
231+
// ...There is no matching method definition in the class, or...
232+
not exists(this.getClass().getMethod(methodName)) or
233+
// ...the called method can access a field.
211234
exists(Method m |
212235
m = this.getClass().methodMayAccessField() |
213-
// TODO: this may be too broad - we haven't limited the call target
214-
// It's likely that the call graph isn't sufficient here, as resolution
215-
// e.g. from ActionView views won't catch everything
216-
m.getName() = call.getMethodName()
236+
// We rely on matching by name here as the call graph might not have
237+
m.getName() = methodName
217238
)
218239
)
219240
}
@@ -298,20 +319,4 @@ private class ActiveRecordInstanceMethodCall extends DataFlow::CallNode {
298319
private ActiveRecordInstance instance;
299320
ActiveRecordInstanceMethodCall() { this.getReceiver() = instance }
300321
ActiveRecordInstance getInstance() { result = instance }
301-
}
302-
303-
private string activeRecordPersistenceInstanceMethodName() {
304-
result =
305-
[
306-
"becomes", "becomes!", "decrement", "decrement!", "delete", "delete!", "destroy", "destroy!",
307-
"destroyed?", "increment", "increment!", "new_record?", "persisted?",
308-
"previously_new_record?", "reload", "save", "save!", "toggle", "toggle!", "touch", "update",
309-
"update!", "update_attribute", "update_column", "update_columns"
310-
]
311-
}
312-
313-
private predicate isCallToBuiltInMethod(MethodCall c) {
314-
c.getMethodName() = activeRecordPersistenceInstanceMethodName() or
315-
c instanceof BasicObjectInstanceMethodCall or
316-
c instanceof ObjectInstanceMethodCall
317-
}
322+
}

ql/lib/codeql/ruby/frameworks/StandardLibrary.qll

Lines changed: 23 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -65,34 +65,42 @@ private predicate isPrivateKernelMethod(string method) {
6565
]
6666
}
6767

68+
string basicObjectInstanceMethodName() {
69+
result in [
70+
"equal?", "instance_eval", "instance_exec", "method_missing", "singleton_method_added",
71+
"singleton_method_removed", "singleton_method_undefined"
72+
]
73+
}
74+
6875
/**
6976
* Instance methods on `BasicObject`, which are available to all classes.
7077
*/
7178
class BasicObjectInstanceMethodCall extends UnknownMethodCall {
7279
BasicObjectInstanceMethodCall() {
73-
this.getMethodName() in [
74-
"equal?", "instance_eval", "instance_exec", "method_missing", "singleton_method_added",
75-
"singleton_method_removed", "singleton_method_undefined"
76-
]
80+
this.getMethodName() = basicObjectInstanceMethodName()
7781
}
7882
}
7983

84+
string objectInstanceMethodName() {
85+
result in [
86+
"!~", "<=>", "===", "=~", "callable_methods", "define_singleton_method", "display",
87+
"do_until", "do_while", "dup", "enum_for", "eql?", "extend", "f", "freeze", "h", "hash",
88+
"inspect", "instance_of?", "instance_variable_defined?", "instance_variable_get",
89+
"instance_variable_set", "instance_variables", "is_a?", "itself", "kind_of?",
90+
"matching_methods", "method", "method_missing", "methods", "nil?", "object_id",
91+
"private_methods", "protected_methods", "public_method", "public_methods", "public_send",
92+
"remove_instance_variable", "respond_to?", "respond_to_missing?", "send",
93+
"shortest_abbreviation", "singleton_class", "singleton_method", "singleton_methods",
94+
"taint", "tainted?", "to_enum", "to_s", "trust", "untaint", "untrust", "untrusted?"
95+
]
96+
}
97+
8098
/**
8199
* Instance methods on `Object`, which are available to all classes except `BasicObject`.
82100
*/
83101
class ObjectInstanceMethodCall extends UnknownMethodCall {
84102
ObjectInstanceMethodCall() {
85-
this.getMethodName() in [
86-
"!~", "<=>", "===", "=~", "callable_methods", "define_singleton_method", "display",
87-
"do_until", "do_while", "dup", "enum_for", "eql?", "extend", "f", "freeze", "h", "hash",
88-
"inspect", "instance_of?", "instance_variable_defined?", "instance_variable_get",
89-
"instance_variable_set", "instance_variables", "is_a?", "itself", "kind_of?",
90-
"matching_methods", "method", "method_missing", "methods", "nil?", "object_id",
91-
"private_methods", "protected_methods", "public_method", "public_methods", "public_send",
92-
"remove_instance_variable", "respond_to?", "respond_to_missing?", "send",
93-
"shortest_abbreviation", "singleton_class", "singleton_method", "singleton_methods",
94-
"taint", "tainted?", "to_enum", "to_s", "trust", "untaint", "untrust", "untrusted?"
95-
]
103+
this.getMethodName() = objectInstanceMethodName()
96104
}
97105
}
98106

ql/lib/codeql/ruby/security/XSS.qll

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -123,7 +123,7 @@ private module Shared {
123123
}
124124

125125
/**
126-
* An additional step that is preserves dataflow in the context of reflected XSS.
126+
* An additional step that is preserves dataflow in the context of XSS.
127127
*/
128128
predicate isAdditionalXSSFlowStep(DataFlow::Node node1, DataFlow::Node node2) {
129129
// node1 is a `locals` argument to a render call...
@@ -229,7 +229,9 @@ module ReflectedXSS {
229229
}
230230
}
231231

232-
// Consider all arbitrary XSS taint steps to be reflected XSS taint steps
232+
/**
233+
* An additional step that is preserves dataflow in the context of reflected XSS.
234+
*/
233235
predicate isAdditionalXSSTaintStep = Shared::isAdditionalXSSFlowStep/2;
234236

235237
/**
@@ -238,7 +240,7 @@ module ReflectedXSS {
238240
class RemoteFlowSourceAsSource extends Source, RemoteFlowSource { }
239241
}
240242

241-
private module OrmTracking {
243+
module OrmTracking {
242244
/**
243245
* A data flow configuration to track flow from finder calls to field accesses.
244246
*/
@@ -285,13 +287,17 @@ module StoredXSS {
285287
}
286288
}
287289

290+
/**
291+
* An additional step that is preserves dataflow in the context of stored XSS.
292+
*/
288293
predicate isAdditionalXSSTaintStep = Shared::isAdditionalXSSFlowStep/2;
289294

290-
private class OrmFieldAsSource extends Source {
295+
private class OrmFieldAsSource extends Source instanceof DataFlow2::CallNode {
291296
OrmFieldAsSource() {
292-
exists(OrmTracking::Configuration subConfig, DataFlow2::Node subSrc |
297+
exists(OrmTracking::Configuration subConfig, DataFlow2::CallNode subSrc, MethodCall call |
293298
subConfig.hasFlow(subSrc, this) and
294-
subSrc.(OrmInstantiation).methodCallMayAccessField(this.asExpr().getExpr())
299+
call = this.asExpr().getExpr() and
300+
subSrc.(OrmInstantiation).methodCallMayAccessField(call.getMethodName())
295301
)
296302
}
297303
}

0 commit comments

Comments
 (0)