diff --git a/python/ql/lib/change-notes/2026-05-19-flask-subclasses.md b/python/ql/lib/change-notes/2026-05-19-flask-subclasses.md new file mode 100644 index 000000000000..e2c5db289c6c --- /dev/null +++ b/python/ql/lib/change-notes/2026-05-19-flask-subclasses.md @@ -0,0 +1,4 @@ +--- +category: minorAnalysis +--- +* `Flask::instance` will now also return instances of subclasses defined in te source tree. Previously, these were filtered out. `Flask::classRef` has been deprecated in favor of `Flask::subclassRef` since it already returned some subclasses. \ No newline at end of file diff --git a/python/ql/lib/semmle/python/frameworks/Flask.qll b/python/ql/lib/semmle/python/frameworks/Flask.qll index f819e8679075..cdfd1b454a1d 100644 --- a/python/ql/lib/semmle/python/frameworks/Flask.qll +++ b/python/ql/lib/semmle/python/frameworks/Flask.qll @@ -71,14 +71,21 @@ module Flask { * See https://flask.palletsprojects.com/en/1.1.x/api/#flask.Flask. */ module FlaskApp { - /** Gets a reference to the `flask.Flask` class. */ - API::Node classRef() { - result = API::moduleImport("flask").getMember("Flask") or + /** + * Gets a reference to the `flask.Flask` class or any subclass. + * + * Deprecated: Use `subclassRef()` instead, this predicate always returned some subclasses. + */ + deprecated API::Node classRef() { result = subclassRef() } + + /** Gets a reference to the `flask.Flask` class or any subclass. */ + API::Node subclassRef() { + result = API::moduleImport("flask").getMember("Flask").getASubclass*() or result = ModelOutput::getATypeNode("flask.Flask~Subclass").getASubclass*() } /** Gets a reference to an instance of `flask.Flask` (a flask application). */ - API::Node instance() { result = classRef().getReturn() } + API::Node instance() { result = subclassRef().getReturn() } } /** @@ -132,7 +139,7 @@ module Flask { API::Node classRef() { result = API::moduleImport("flask").getMember("Response") or - result = [FlaskApp::classRef(), FlaskApp::instance()].getMember("response_class") + result = [FlaskApp::subclassRef(), FlaskApp::instance()].getMember("response_class") or result = ModelOutput::getATypeNode("flask.Response~Subclass").getASubclass*() } diff --git a/python/ql/src/meta/ClassHierarchy/Find.ql b/python/ql/src/meta/ClassHierarchy/Find.ql index 2c474cb21028..e13c683b6f10 100644 --- a/python/ql/src/meta/ClassHierarchy/Find.ql +++ b/python/ql/src/meta/ClassHierarchy/Find.ql @@ -351,7 +351,7 @@ class DjangoHttpRequest extends FindSubclassesSpec { class FlaskClass extends FindSubclassesSpec { FlaskClass() { this = "flask.Flask~Subclass" } - override API::Node getAlreadyModeledClass() { result = Flask::FlaskApp::classRef() } + override API::Node getAlreadyModeledClass() { result = Flask::FlaskApp::subclassRef() } } class FlaskBlueprint extends FindSubclassesSpec { diff --git a/python/ql/test/experimental/meta/InlineInstanceTest.qll b/python/ql/test/experimental/meta/InlineInstanceTest.qll new file mode 100644 index 000000000000..d89f6c5faad7 --- /dev/null +++ b/python/ql/test/experimental/meta/InlineInstanceTest.qll @@ -0,0 +1,29 @@ +/** + * Defines a InlineExpectationsTest for class instances, that is, + * for any API::Node that is an instance of a class (e.g. `Flask`). + */ + +import python +import semmle.python.ApiGraphs +import utils.test.InlineExpectationsTest +private import semmle.python.dataflow.new.internal.PrintNode + +signature API::Node getInstanceSig(); + +module MakeInlineInstanceTest { + private module InlineInstanceTest implements TestSig { + string getARelevantTag() { result = "instance" } + + predicate hasActualResult(Location location, string element, string tag, string value) { + exists(location.getFile().getRelativePath()) and + exists(API::Node instance | instance = getInstance() | + location = instance.getLocation() and + element = prettyNode(instance.asSource()) and + value = "" and + tag = "instance" + ) + } + } + + import MakeTest +} diff --git a/python/ql/test/library-tests/frameworks/flask/InlineInstanceTest.expected b/python/ql/test/library-tests/frameworks/flask/InlineInstanceTest.expected new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/python/ql/test/library-tests/frameworks/flask/InlineInstanceTest.ql b/python/ql/test/library-tests/frameworks/flask/InlineInstanceTest.ql new file mode 100644 index 000000000000..10dff72385a3 --- /dev/null +++ b/python/ql/test/library-tests/frameworks/flask/InlineInstanceTest.ql @@ -0,0 +1,8 @@ +import python +import semmle.python.frameworks.Flask +import semmle.python.ApiGraphs +import experimental.meta.InlineInstanceTest + +API::Node getInstance() { result = Flask::FlaskApp::instance() } + +import MakeInlineInstanceTest diff --git a/python/ql/test/library-tests/frameworks/flask/flask_subclass.py b/python/ql/test/library-tests/frameworks/flask/flask_subclass.py new file mode 100644 index 000000000000..0fccd847266f --- /dev/null +++ b/python/ql/test/library-tests/frameworks/flask/flask_subclass.py @@ -0,0 +1,14 @@ +from flask import Flask + + +class Sub(Flask): + def __init__(self, *args, **kwargs): + Flask.__init__(self, *args, **kwargs) + + +app = Sub(__name__) # $ instance + + +@app.route("/") +def hello(): + return "world" \ No newline at end of file diff --git a/python/ql/test/library-tests/frameworks/flask/old_test.py b/python/ql/test/library-tests/frameworks/flask/old_test.py index 4c1ee89b5300..48b3b7c7f29d 100644 --- a/python/ql/test/library-tests/frameworks/flask/old_test.py +++ b/python/ql/test/library-tests/frameworks/flask/old_test.py @@ -1,7 +1,7 @@ import flask from flask import Flask, request, make_response -app = Flask(__name__) +app = Flask(__name__) # $ instance @app.route("/") # $ routeSetup="/" def hello_world(): # $ requestHandler diff --git a/python/ql/test/library-tests/frameworks/flask/response_test.py b/python/ql/test/library-tests/frameworks/flask/response_test.py index e775239d6423..7491c6d3e9c7 100644 --- a/python/ql/test/library-tests/frameworks/flask/response_test.py +++ b/python/ql/test/library-tests/frameworks/flask/response_test.py @@ -3,7 +3,7 @@ from flask import Flask, make_response, jsonify, Response, request, redirect from werkzeug.datastructures import Headers -app = Flask(__name__) +app = Flask(__name__) # $ instance @app.route("/html1") # $ routeSetup="/html1" diff --git a/python/ql/test/library-tests/frameworks/flask/routing_test.py b/python/ql/test/library-tests/frameworks/flask/routing_test.py index 1bd8de5e7dee..837cb34d293e 100644 --- a/python/ql/test/library-tests/frameworks/flask/routing_test.py +++ b/python/ql/test/library-tests/frameworks/flask/routing_test.py @@ -1,7 +1,7 @@ import flask from flask import Flask, make_response -app = Flask(__name__) +app = Flask(__name__) # $ instance SOME_ROUTE = "/some/route" diff --git a/python/ql/test/library-tests/frameworks/flask/save_uploaded_file.py b/python/ql/test/library-tests/frameworks/flask/save_uploaded_file.py index 502d4cdcbc28..691029844d0d 100644 --- a/python/ql/test/library-tests/frameworks/flask/save_uploaded_file.py +++ b/python/ql/test/library-tests/frameworks/flask/save_uploaded_file.py @@ -1,5 +1,5 @@ from flask import Flask, request -app = Flask(__name__) +app = Flask(__name__) # $ instance @app.route("/save-uploaded-file") # $ routeSetup="/save-uploaded-file" def test_taint(): # $ requestHandler diff --git a/python/ql/test/library-tests/frameworks/flask/taint_test.py b/python/ql/test/library-tests/frameworks/flask/taint_test.py index 85637d60f428..9cdee60ef584 100644 --- a/python/ql/test/library-tests/frameworks/flask/taint_test.py +++ b/python/ql/test/library-tests/frameworks/flask/taint_test.py @@ -1,5 +1,5 @@ from flask import Flask, request, render_template_string, stream_template_string -app = Flask(__name__) +app = Flask(__name__) # $ instance @app.route("/test_taint//") # $ routeSetup="/test_taint//" def test_taint(name = "World!", number="0", foo="foo"): # $ requestHandler routedParameter=name routedParameter=number diff --git a/python/ql/test/library-tests/frameworks/flask/template_test.py b/python/ql/test/library-tests/frameworks/flask/template_test.py index b10dd3e6645a..2c482e9cb82d 100644 --- a/python/ql/test/library-tests/frameworks/flask/template_test.py +++ b/python/ql/test/library-tests/frameworks/flask/template_test.py @@ -1,5 +1,5 @@ from flask import Flask, Response, stream_with_context, render_template_string, stream_template_string -app = Flask(__name__) +app = Flask(__name__) # $ instance @app.route("/a") # $ routeSetup="/a" def a(): # $ requestHandler