Skip to content

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

Merged
merged 6 commits into from
Oct 7, 2022

Conversation

hmac
Copy link
Contributor

@hmac hmac commented Sep 22, 2022

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.

@github-actions github-actions bot added the Ruby label Sep 22, 2022
@hmac hmac force-pushed the hmac/actioncontroller-parameters branch 2 times, most recently from cf80103 to 38999a3 Compare September 26, 2022 20:54
@hmac hmac marked this pull request as ready for review September 26, 2022 22:19
@hmac hmac requested a review from a team as a code owner September 26, 2022 22:19
or
exists(DataFlow::LocalSourceNode prev | prev.flowsTo(this))
}
}
Copy link
Contributor Author

@hmac hmac Sep 27, 2022

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 TypeModels will run into this problem.

Copy link
Contributor

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.

Copy link
Contributor Author

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()
  }
}

Copy link
Contributor

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?

Copy link
Contributor Author

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.

Copy link
Contributor

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.

Copy link
Contributor Author

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 👍

alexrford
alexrford previously approved these changes Sep 29, 2022
Copy link
Contributor

@alexrford alexrford left a 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))
Copy link
Contributor

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?

Copy link
Contributor Author

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 |
Copy link
Contributor

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
Copy link
Contributor

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])?

Copy link
Contributor Author

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.

Copy link
Contributor Author

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.

Copy link
Contributor Author

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.

Copy link
Contributor

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
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same comment as above.

hmac added 5 commits October 3, 2022 09:45
Add flow summaries for methods on ActionController::Parameters,
which mostly propagate taint from receiver to return value.
@hmac hmac force-pushed the hmac/actioncontroller-parameters branch from fd18fa2 to 236b628 Compare October 3, 2022 01:06
Copy link
Contributor

@hvitved hvitved left a 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.

@hmac
Copy link
Contributor Author

hmac commented Oct 6, 2022

@hvitved DCA shows a 5% slowdown on opal/opal but I don't know why, since there are no ActionController::Parameters instances in that project. Otherwise everything looks OK.

@hvitved
Copy link
Contributor

hvitved commented Oct 7, 2022

DCA shows a 5% slowdown on opal/opal but I don't know why

I think that may just be noise. So go ahead and merge.

@hmac hmac merged commit 75cb0ef into github:main Oct 7, 2022
@hmac hmac deleted the hmac/actioncontroller-parameters branch October 10, 2022 08:10
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants