Skip to content
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

Python: New type-tracking based call-graph #11376

Open
wants to merge 75 commits into
base: main
Choose a base branch
from

Conversation

RasmusWL
Copy link
Member

@RasmusWL RasmusWL commented Nov 22, 2022

No description provided.

RasmusWL added 30 commits Nov 22, 2022
These tests are not relevant anymore 💪
So diff can make more sense when introducing blank state for type-tracking based call-graph
That does absolutely nothing so far, but compiles
After solving merge conflict
Also changed the definition of a relevant call-target, so it's only what
is in the actual source code, which is what we want in the future! (so
what we're designing type-tracking to handle)

I also changed terminology from `callee` to `target`. It felt more
natural this way in my opinion.
This commit is a squash of 80 other commits. While developing, things
changed majorly 2-3 times, and it just wasn't feasible to go back and
write a really nice commit history.

My apologies for this HUGE commit.

Also, later on this is where I solved merge conflicts after flow-summaries
PR was merged.

For your amusement, I've included the original commit messages below.

Python: Add proper argument/parameter positions

Python: Handle normal function calls

Python: Reduce dataflow-consistency warnings

Previously there was a lot of failures for `uniqueEnclosingCallable` and
`argHasPostUpdate`

Removing the override of `getEnclosingCallable` in ParameterNode is
probably the most controversial... although from my point of view it's a
change for the better, since we're able to provide data-flow
ParameterNodes for more of the AST parameter nodes.

Python: Adjust `dataflow/calls` test

Python: Implement `isParameterOf`/`argumentOf`/`OutNode`

This makes the tests under `dataflow/basic` work as well 👍

(initially I had these as separate commits, but it felt like it was too much noise)

Python: Accept fix for `dataflow/consistency`

Python: Changes to `coverage/argumentRoutingTest.ql`

Notice we gain a few new resolved arguments.

We loose out on stuff due to:

1. not handling `*` or `**` in either arguments/parameters (yet)
2. not handling special calls (yet)

Python: Small fix for `TestUtil/RoutingTest.qll`

Since the helper predicates do not depend on this, moved outside class.

Python: Accept changes to `dataflow/coverage/NormalDataflowTest.ql`

Most of this is due to:

- not handling any kinds of methods yet
- not handling `*` or `**`

Python: Small investigation of `test_deep_callgraph`

Python: Accept changes to `coverage/localFlow.ql`

I don't fully understand why the .expected file changed.

Since we still have the desired flow, I'm not going to worry too much
about it.

with this commit, the `dataflow/coverage` tests passes 👍

Python: Minor doc update

Python: Add staticmethod/classmethod to `dataflow/calls`

Python: Handle method calls on class instances

without trying to deal with any class inheritance, or
staticmethod/classmethod at all.

Notice that with this change, we only have a DataFlowCall for the calls
that we can actually resolve. I'm not 100% sure if we need to add a
`UnresolvedCall` subclass of `DataFlowCall` for MaD in the future, but
it should be easy to do.

I'm still unsure about the value of `classesCallGraph`, but have just
accepted the changes.

Python: Handle direct method calls `C.foo(C, arg0)`

Python: Handle `@staticmethod`

Python: Handle class method calls... but the code is shit

WIP todo

Rewrite method calls to be better

also fixed a problem with `self` being an argument to the `x.staticmethod()` call :|

Python: Add subclass tests

Python: Split `class_advanced` test

Python: Rewrite call-graph tests to be inline expectation (1/2)

This adds inline expectations, next commit will remove old annotations
code... but I thought it would be easier to review like this.

Minor fixup

Python: Add simple subclass support

Python: more precise subclass lookup

Still not 100% precise.. but it's better

New ambiguous

Python: Add test for `self.m()` and `cls.m()` calls

Python: Handle `self.m()` and `cls.m()` calls

Python: Add tests for `__init__` and `__new__`

Python: Handle class calls

Python: Fix `self` argument passing for class calls

Now field-flow tests also pass 💪 (although the crosstalk
fieldflow test changes were due to this specific commit)

I also copied much of the setup for pre/post update nodes from Ruby,
specifically having the abstract `PostUpdateNodeImpl` in DataFlowPrivate
seemed like a nice change.

Same for the setup with `TNode` definition having the specification
directly in the body, instead of a `NeedsSyntheticPostUpdateNode` class.

Python: Add new crosstalk test WIP

Maybe needs a bit of refactoring, and to see how it all behaves with points-to

Python: Add `super()` call-graph tests

Python: Refactor MethodCall char-pred

In anticipation of supporting `super(MyClass, self).foo()`, where the
`self` argument doesn't come from an AttrNode, but from the second
argument to super.

Without `pragma[inline]` the optimizer found a terrible join-order --
this won't guarantee a good join-order for the future, but for now it
was just so simple and could let me move on with life.

Python: Add basic `super()` support

I debated a little (with myself) whether I should really do
`superTracker`, but I thought "why not" and just rolled with it. I did
not confirm whether it was actually needed anywhere, that is if anyone
does `ref = super; ref().foo()` -- although I certainly doubt it's very
wide-spread.

Python: InlineCallGraphTest: Allow non-unique callable name in different files

Python: more MRO tests

Python: Add MRO approximation for `super()`

Although it's not 100% accurate, it seems to be on level with the one in
points-to.

Python: Remove some spurious targets for direct calls

removal of TODO from refactoring

remove TODOs class call support

Python: Add contrived subclass call example

Python: Remove more spurious call targets

NOTE: I initially forgot to use
`findFunctionAccordingToMroKnownStartingClass` instead of
`findFunctionAccordingToMro` for __init__ and __new__, and since I did
make that mistake myself, I wanted to add something to the test to
highlight this fact, and make it viewable by PR reviewer... this will be
fixed in the next commit.

Python: Proper fix for spurious __init__ targets

Python: Add call-graph example of class decorator

Python: Support decorated classes in new call-graph

Python: Add call-graph tests for `type(obj).meth()`

Python: support `type(obj).meth()`

Python: Add test for callable defined in function

Python: Add test for callable as argument

Current'y we don't find these with type-tracking, which is super
mysterious. I did check that we have proper flow from the arguments to
the parameters.

Python: Found problem for callable as argument :| MAJOR WIP

WIP commit

IT WORKS AGAIN (but terrible performance)

remove pragma[inline]

remove oops

Fix performance problem

I tried to optimize it even further, but I didn't end up achieving anything :|

Fix call-graph comparison

add comparison version with easy lookup

incomplete missing call-graph tests

unhandled tests

trying to replicate missing call-edge due to missing imports ... but it's hard

also seems to be problems with the inline-expectation-value that I used, seems like it has both missing/unexpected results with same value

Python: Add import-problem test

Python: Add shadowing problem

some cleanup of rewrite fix

a little more cleanup

Add consistency queries to call-graph tests

Python: Add post-update nodes for `self` in implicit `super()` uses

But we do need to discuss whether this is the right approach :O

Fix for field-flow tests

This came from more precise argument passing

Fixed results in type-tracking

Comes from better argument passing with super() and handling of
functions with decorators

fix of inline call graph tests

Fixup call annotation test

Many minor cleanups/fixes

NewNormalCall -> NormalCall

Python: Major restructuring + qldoc writing

Python: Accept changes from pre/post update node .toString changes

Python: Reduce `super` complexity !! WIP !!

Python: Only pass self-reference if in same enclosing-callable

Python: Add call-graph test with nested class

This was inspired by the ImpliesDataflow test that showed missing flow
for q_super, but at least for the call-graph, I'm not able to reproduce
this missing result :|

Python: Restrict `super()` to function defined directly on class

Python: Accept fixes to ImpliesDataflow

Python: Expand field-flow crosstalk tests
Since if you had tornado installed, we would follow imports and have
results from those files as well :|
The output might end up being slightly more noisy since we don't
collapse positional and keyword arguments when the external target
function is included in the database, but this aligns with our long-term
goal of not doing that anymore, so I think it's fine.
…allGraphTest`

Since I was very confused about no results for __call__, I tried to see
whether I had cheated by making the comparison too unfair. But it didn't
seem to be the case.
Since `DataFlowPrivate::DataFlowCall` only exists for calls resolved to
a function, we didn't have any results before... but allowing any call
helps things!
On pallets/flask, this reduced the number of tuples from
100866 results => 33060 results
namely the variable access mentioned in
github#10171
For now this is JUST from `**kwargs` in arguments, to `**kwargs`
parameters, and this part is based on field-flow

Note that dataflow-library complains about missing post update nodes for
these. This needs to be ignored, since post update nodes for `**kwargs`
arguments doesn't make sense, it's not possible to alter the dictionary
inside the method.
And ignore post-update nodes for `**kwargs` arguments
When resolving merge conflict after flow-summaries was merged, this is
the original commit where I introduced ParameterNodeImpl, so this is the
commit where differences in that implementation was committed...

I removed TParameterNode, since I could not see we we gain anything from
having it.
RasmusWL added 21 commits Nov 22, 2022
Also needed to fix up `TestUtil/UnresolvedCalls.qll` after a bad merge
conflict resolution. Since all calls are now DataFlowCall, and not JUST
the ones that can be resolved, we need to put in the restriction that
the callable can also be resolved.
Using the object from `MethodCallNode` meant that in the code below,
`lib` from the import expression would be considered a self argument

(this showed up in dataflow-consistency query results, that were not
comitted... sorry)

```
from lib import func
func()
```
I've been living dangerously with that assumption :|
But we don't want to keep this, this commit is just to show why we need a fix :)
Since it has the same problem of showing sinks inside the extracted
stdlib
This must mean that we did not have this flow with the old call-graph,
which means the new call-graph is doing a better job (yay).
We DON'T want to recompute these ones for sure!
With points-to not being used for the call-graph any longer, it's time
to split them.
I didn't do any performance investigation on this, since it just seems
so much like the right approach.
I did check, and this was not a problem with the old call-graph on main!

I'm absolutely baffled!
@github-actions
Copy link
Contributor

github-actions bot commented Nov 22, 2022

QHelp previews:

python/ql/src/Security/CWE-020-ExternalAPIs/ExternalAPIsUsedWithUntrustedData.qhelp

Frequency counts for external APIs that are used with untrusted data

Using unsanitized untrusted data in an external API can cause a variety of security issues. This query reports external APIs that are used with untrusted data, along with how frequently the API is used, and how many unique sources of untrusted data flow to this API. This query is designed primarily to help identify which APIs may be relevant for security analysis of this application.

An external API is defined as a call to a method that is not defined in the source code, and is not modeled as a taint step in the default taint library. External APIs may be from the Python standard library or dependencies. The query will report the fully qualified name, along with [position index] or [keyword name], to indicate the argument passing the untrusted data.

Note that an excepted sink might not be included in the results, if it also defines a taint step. This is the case for pickle.loads which is a sink for the Unsafe Deserialization query, but is also a taint step for other queries.

Note: Compared to the Java version of this query, we currently do not give special care to methods that are overridden in the source code.

Recommendation

For each result:

  • If the result highlights a known sink, no action is required.
  • If the result highlights an unknown sink for a problem, then add modeling for the sink to the relevant query.
  • If the result represents a call to an external API which transfers taint, add the appropriate modeling, and re-run the query to determine what new results have appeared due to this additional modeling.
    Otherwise, the result is likely uninteresting. Custom versions of this query can extend the SafeExternalAPI class and specify getSafeCallable to exclude known safe external APIs from future analysis.

Example

If the query were to return the API flask.make_response [param 0] then we should first consider whether this a security relevant sink. In this case, this is making a HTTP response, so we should consider whether this is an XSS sink. If it is, we should confirm that it is handled by the XSS query.

If the query were to return the API base64.decodebytes [param 0], then this should be reviewed as a possible taint step, because tainted data would flow from the 0th argument to the result of the call.

Note that both examples are correctly handled by the standard taint tracking library and XSS query.

References

  • Common Weakness Enumeration: CWE-20.
python/ql/src/Security/CWE-020-ExternalAPIs/UntrustedDataToExternalAPI.qhelp

Untrusted data passed to external API

Using unsanitized untrusted data in an external API can cause a variety of security issues. This query reports external APIs that use untrusted data. The results are not filtered so that you can audit all examples. The query provides data for security reviews of the application and you can also use it to identify external APIs that should be modeled as either taint steps, or sinks for specific problems.

An external API is defined as a call to a method that is not defined in the source code, and is not modeled as a taint step in the default taint library. External APIs may be from the Python standard library or dependencies. The query will report the fully qualified name, along with [position index] or [keyword name], to indicate the argument passing the untrusted data.

Note that an excepted sink might not be included in the results, if it also defines a taint step. This is the case for pickle.loads which is a sink for the Unsafe Deserialization query, but is also a taint step for other queries.

Note: Compared to the Java version of this query, we currently do not give special care to methods that are overridden in the source code.

Recommendation

For each result:

  • If the result highlights a known sink, confirm that the result is reported by the relevant query, or that the result is a false positive because this data is sanitized.
  • If the result highlights an unknown sink for a problem, then add modeling for the sink to the relevant query, and confirm that the result is either found, or is safe due to appropriate sanitization.
  • If the result represents a call to an external API that transfers taint, add the appropriate modeling, and re-run the query to determine what new results have appeared due to this additional modeling.
    Otherwise, the result is likely uninteresting. Custom versions of this query can extend the SafeExternalAPI class and specify getSafeCallable to exclude known safe external APIs from future analysis.

Example

In this first example, a request parameter is read from the Flask request and then ultimately used in a call to the flask.make_response external API:

from flask import Flask, request, make_response
app = Flask(__name__)

@app.route("/xss")
def xss():
    username = request.args.get("username")
    return make_response("Hello {}".format(username))

This is an XSS sink. The XSS query should therefore be reviewed to confirm that this sink is appropriately modeled, and if it is, to confirm that the query reports this particular result, or that the result is a false positive due to some existing sanitization.

In this second example, again a request parameter is read from the Flask request.

import base64
import pickle

from flask import Flask, request, make_response
app = Flask(__name__)

@app.route("/example")
def profile():
    raw_data = request.args.get("data").encode('utf-8')
    data = base64.decodebytes(raw_data)
    obj = pickle.loads(data)
    ...

If the query reported the call to base64.decodebytes on line 10, this would suggest that this external API is not currently modeled as a taint step in the taint tracking library. The next step would be to model this as a taint step, then re-run the query to determine what additional results might be found. In this example, the result of the Base64 decoding is pickled, which can result in remote code execution due to unsafe deserialization.

Note that both examples are correctly handled by the standard taint tracking library and Unsafe Deserialization query.

References

  • Common Weakness Enumeration: CWE-20.

* ...
* ```
*/
private predicate dictSplatParameterNodeClearStep(ParameterNode n, DictionaryElementContent c) {

Check warning

Code scanning / CodeQL

Missing QLDoc for parameter Warning

The QLDoc has no documentation for c, or n, but the QLDoc mentions kwargs
*/
class RestArgumentRoutingConfig extends DataFlow::Configuration {
int argNumber;
/** Bad flow from `arg<n>` to `SINK<N>_F` */

Check warning

Code scanning / CodeQL

Class QLDoc style. Warning

The QLDoc for a class should start with 'A', 'An', or 'The'.
exists(CallNode call |
call.getFunction().(NameNode).getId() = "SINK" + argNumber and
node.(DataFlow::CfgNode).getNode() = call.getAnArg()
/** Bad flow from `arg<n>` to `SINK<M>` or `SINK<M>_F`, where `n != m`. */

Check warning

Code scanning / CodeQL

Class QLDoc style. Warning

The QLDoc for a class should start with 'A', 'An', or 'The'.
* resolve the call to a known target (since the only super class might be the
* builtin `object`, so we never have the implementation of `__new__` in the DB).
*/
predicate fromSuperNewCall(CallNode call, Class classUsedInSuper, AttrRead attr, Node self) {

Check warning

Code scanning / CodeQL

Missing QLDoc for parameter Warning

The QLDoc has no documentation for attr, or classUsedInSuper, or self, but the QLDoc mentions object
The rest will be ignored.
@RasmusWL RasmusWL marked this pull request as ready for review Nov 23, 2022
@RasmusWL RasmusWL requested a review from a team as a code owner Nov 23, 2022
This means points-to is no longer evaluated for sql injection 🎉

Thanks @asgerf 💪
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.

None yet

1 participant