diff --git a/python/ql/lib/change-notes/2022-06-15-class-decorator-api-subclass.md b/python/ql/lib/change-notes/2022-06-15-class-decorator-api-subclass.md new file mode 100644 index 00000000000..04beefb14b6 --- /dev/null +++ b/python/ql/lib/change-notes/2022-06-15-class-decorator-api-subclass.md @@ -0,0 +1,4 @@ +--- +category: minorAnalysis +--- +* Change `.getASubclass()` on `API::Node` so it allows to follow subclasses even if the class has a class decorator. diff --git a/python/ql/lib/semmle/python/ApiGraphs.qll b/python/ql/lib/semmle/python/ApiGraphs.qll index fcb89e5f866..1023347b832 100644 --- a/python/ql/lib/semmle/python/ApiGraphs.qll +++ b/python/ql/lib/semmle/python/ApiGraphs.qll @@ -591,8 +591,18 @@ module API { or // Subclassing a node lbl = Label::subclass() and - exists(DataFlow::Node superclass | pred.flowsTo(superclass) | - ref.asExpr().(PY::ClassExpr).getABase() = superclass.asExpr() + exists(PY::ClassExpr clsExpr, DataFlow::Node superclass | pred.flowsTo(superclass) | + clsExpr.getABase() = superclass.asExpr() and + // Potentially a class decorator could do anything, but we assume they are + // "benign" and let subclasses edges flow through anyway. + // see example in https://github.com/django/django/blob/c2250cfb80e27cdf8d098428824da2800a18cadf/tests/auth_tests/test_views.py#L40-L46 + ( + not exists(clsExpr.getADecorator()) and + ref.asExpr() = clsExpr + or + ref.asExpr() = clsExpr.getADecoratorCall() and + not exists(PY::Call otherDecorator | otherDecorator.getArg(0) = ref.asExpr()) + ) ) or // awaiting diff --git a/python/ql/test/library-tests/ApiGraphs/py3/deftest2.py b/python/ql/test/library-tests/ApiGraphs/py3/deftest2.py index 276b5ca3212..ef15ece04f6 100644 --- a/python/ql/test/library-tests/ApiGraphs/py3/deftest2.py +++ b/python/ql/test/library-tests/ApiGraphs/py3/deftest2.py @@ -30,5 +30,5 @@ def my_class_decorator(cls): class MyViewWithDecorator(View): #$ use=moduleImport("flask").getMember("views").getMember("View").getASubclass() pass -class SubclassFromDecorated(MyViewWithDecorator): #$ MISSING: use=moduleImport("flask").getMember("views").getMember("View").getASubclass().getASubclass() +class SubclassFromDecorated(MyViewWithDecorator): #$ use=moduleImport("flask").getMember("views").getMember("View").getASubclass().getASubclass() pass