-
Notifications
You must be signed in to change notification settings - Fork 1.7k
Ruby: Model flow through ActionController::Parameters #10538
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
cf80103
to
38999a3
Compare
or | ||
exists(DataFlow::LocalSourceNode prev | prev.flowsTo(this)) | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@asgerf just FYI, I wanted to define a TypeModel
here, so I could write the type and flow summaries in MaD form:
class ParamsInstanceType extends ModelInput::TypeModel {
override DataFlow::LocalSourceNode getASource(string package, string type) {
package = "actioncontroller" and
type = "ActionController::Parameters" and
result instanceof ParamsCall
}
}
But this created non-monotonic recursion errors, I think because ParamsCall
uses API graphs (via ActionControllerContextCall
). I don't know if it's possible to work around/fix this, but thought you would want to know. I expect many use cases for TypeModel
s will run into this problem.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That is inevitable, I'm afraid. If ParamsCall
depends on API graphs, shouldn't you already have an API node for it? In that case, using the API graph members will have to be good enough.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's a bit more convoluted than that:
abstract class ParamsCall extends MethodCall {
ParamsCall() { this.getMethodName() = "params" }
}
private class ActionControllerParamsCall extends ActionControllerContextCall, ParamsCall { }
private class ActionControllerContextCall extends MethodCall {
private ActionControllerControllerClass controllerClass;
ActionControllerContextCall() {
this.getReceiver() instanceof SelfVariableAccess and
this.getEnclosingModule() = controllerClass
}
}
class ActionControllerControllerClass extends ClassDeclaration {
ActionControllerControllerClass() {
this.getSuperclassExpr() =
[
API::getTopLevelMember("ActionController").getMember("Base"),
API::getTopLevelMember("ApplicationController")
].getASubclass().getAValueReachableFromSource().asExpr().getExpr()
}
}
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actually on second thought it seems really easy to add #10603. Would this work for you?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't think that helps here (though I could be wrong). We have API nodes for ActionControllerControllerClass
, but not ParamsCall
, which is abstract. In fact there's a second contribution to this class from calls to params
in ERB files (see ActionView.qll
), and in that case we have no API nodes at all.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
OK, I've taken a look at that model and the problem arises when we're using API graphs to populate concepts that are based on AST nodes. Having to step outside API graphs should be considered tech debt, and not be a common case, and I believe Ruby's API graphs need to be developed a bit further for this to work. In particular we'll need to add MethodDefinitionNode
(in JS this would correspond to a MkDef
wrapping a DataFlow::FunctionNode
) and something like the MkClassInstance
node from JS. I'll look into that, but it will not arrive in time for this PR.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That sounds like the right sort of thing we'd need for a lot of this modelling 👍
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Really nice, the changes make sense and the new results look legit!
call.getMethodName() = paramsMethodReturningParamsInstance() | ||
) | ||
or | ||
exists(DataFlow::LocalSourceNode prev | prev.flowsTo(this)) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Shouldn't prev
be restricted?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah yes, prev
should be a ParamsInstance
! 🤦
ParamsInstance() { | ||
this.asExpr().getExpr() instanceof ParamsCall | ||
or | ||
exists(DataFlow::CallNode call | call = this | |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I tend to write this = any(DataFlow::CallNode call
instead.
} | ||
|
||
override predicate propagatesFlowExt(string input, string output, boolean preservesValue) { | ||
input = ["Argument[self]", "Argument[0]"] and |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Presumably, only Argument[self]
when exists(ParamsInstance i | i.asExpr().getExpr() = result.getReceiver())
(and similarly for Argument[0]
)?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't think so, though I may be misunderstanding you.
getACall
already restricts us to cases where either the receiver is a ParamsInstance
or arg0 is a ParamsInstance
. merge
is a stdlib method on Hash
. In addition, activesupport adds reverse_merge
and with_defaults
to Hash
. Thus we get the following behaviour:
params = ActionController::Parameters.new({a: 1})
=> #<ActionController::Parameters {"a"=>1} permitted: false>
params.permit! # required for some of these to work
=> #<ActionController::Parameters {"a"=>1} permitted: true>
params.merge({b:2})
=> #<ActionController::Parameters {"a"=>1, "b"=>2} permitted: true>
{b:2}.merge(params)
=> {:b=>2, "a"=>1}
params.reverse_merge({b:2})
=> #<ActionController::Parameters {"b"=>2, "a"=>1} permitted: true>
{b:2}.reverse_merge(params)
=> #<ActionController::Parameters {"a"=>1, "b"=>2} permitted: true>
params.with_defaults({b:2})
=> #<ActionController::Parameters {"b"=>2, "a"=>1} permitted: true>
{b:2}.with_defaults(params)
=> #<ActionController::Parameters {"a"=>1, "b"=>2} permitted: true>
params2 = ActionController::Parameters.new({b:2}).tap(&:permit!)
=> #<ActionController::Parameters {"b"=>2} permitted: false>
params.merge(params2)
=> #<ActionController::Parameters {"a"=>1, "b"=>2} permitted: true>
params2.merge(params)
=> #<ActionController::Parameters {"b"=>2, "a"=>1} permitted: true>
params.reverse_merge(params2)
=> #<ActionController::Parameters {"b"=>2, "a"=>1} permitted: true>
params2.reverse_merge(params)
=> #<ActionController::Parameters {"a"=>1, "b"=>2} permitted: true>
params.with_defaults(params2)
=> #<ActionController::Parameters {"b"=>2, "a"=>1} permitted: true>
params2.with_defaults(params)
=> #<ActionController::Parameters {"a"=>1, "b"=>2} permitted: true>
In all cases, taint is propagated from receiver and arg0 to the return value, regardless of which is the ParamsInstance
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Lots of these aren't covered in the tests, so I'll add cases for them.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actually the only missing cases are merging two params instances, but in practice these would both be tainted anyway so it's not so important to test I think.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
OK, just wanted to double check that there was not only supposed to be flow from the argument that is an instance of ParamsInstance
.
} | ||
|
||
override predicate propagatesFlowExt(string input, string output, boolean preservesValue) { | ||
input = ["Argument[self]", "Argument[0]"] and |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Same comment as above.
Add flow summaries for methods on ActionController::Parameters, which mostly propagate taint from receiver to return value.
fd18fa2
to
236b628
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM. I have started a new DCA run; let's see what it says before merging.
@hvitved DCA shows a 5% slowdown on |
I think that may just be noise. So go ahead and merge. |
Add flow summaries for methods on
ActionController::Parameters
,which mostly propagate taint from receiver to return value.
Evaluation shows that we catch a couple more TPs due to flow through parameters now reaching some sinks.