From 8647073433cb518fc450ee0ecc504d7b9154a21b Mon Sep 17 00:00:00 2001
From: Joe Farebrother
Date: Tue, 5 Nov 2024 17:06:53 +0000
Subject: [PATCH 01/20] Copy template injection to standard pack + add jinja
sinks
---
python/ql/lib/semmle/python/Concepts.qll | 26 ++++++++++
.../lib/semmle/python/frameworks/Jinja2.qll | 0
.../TemplateInjectionCustomizations.qll | 50 +++++++++++++++++++
.../dataflow/TemplateInjectionQuery.qll | 22 ++++++++
.../CWE-074/TemplateConstructionConcept.qll | 7 ++-
.../TemplateInjectionCustomizations.qll | 4 +-
6 files changed, 106 insertions(+), 3 deletions(-)
create mode 100644 python/ql/lib/semmle/python/frameworks/Jinja2.qll
create mode 100644 python/ql/lib/semmle/python/security/dataflow/TemplateInjectionCustomizations.qll
create mode 100644 python/ql/lib/semmle/python/security/dataflow/TemplateInjectionQuery.qll
diff --git a/python/ql/lib/semmle/python/Concepts.qll b/python/ql/lib/semmle/python/Concepts.qll
index cc0712d181b7..cf6f0214496f 100644
--- a/python/ql/lib/semmle/python/Concepts.qll
+++ b/python/ql/lib/semmle/python/Concepts.qll
@@ -861,6 +861,32 @@ class LdapFilterEscaping extends Escaping {
LdapFilterEscaping() { super.getKind() = Escaping::getLdapFilterKind() }
}
+/**
+ * A data-flow node that constructs a template in a templating engine.
+ *
+ * Extend this class to refine existing API models. If you want to model new APIs,
+ * extend `TemplateConstruction::Range` instead.
+ */
+class TemplateConstruction extends DataFlow::Node instanceof TemplateConstruction::Range {
+ /** Gets the argument that specifies the template source. */
+ DataFlow::Node getSourceArg() { result = super.getSourceArg() }
+}
+
+/** Provides classes for modelling template construction APIs. */
+module TemplateConstruction {
+ /**
+ * A data-flow node that constructs a template in a templating engine.
+ *
+ * Extend this class to model new APIs. If you want to refine existing API models,
+ * extend `TemplateConstruction` instead.
+ */
+ abstract class Range extends DataFlow::Node {
+ /** Gets the argument that specifies the template source. */
+ abstract DataFlow::Node getSourceArg();
+ }
+}
+
+
/** Provides classes for modeling HTTP-related APIs. */
module Http {
/** Gets an HTTP verb, in upper case */
diff --git a/python/ql/lib/semmle/python/frameworks/Jinja2.qll b/python/ql/lib/semmle/python/frameworks/Jinja2.qll
new file mode 100644
index 000000000000..e69de29bb2d1
diff --git a/python/ql/lib/semmle/python/security/dataflow/TemplateInjectionCustomizations.qll b/python/ql/lib/semmle/python/security/dataflow/TemplateInjectionCustomizations.qll
new file mode 100644
index 000000000000..e61d55253090
--- /dev/null
+++ b/python/ql/lib/semmle/python/security/dataflow/TemplateInjectionCustomizations.qll
@@ -0,0 +1,50 @@
+/**
+ * Provides default sources, sinks and sanitizers for detecting
+ * "template injection"
+ * vulnerabilities, as well as extension points for adding your own.
+ */
+
+private import python
+private import semmle.python.dataflow.new.DataFlow
+private import semmle.python.Concepts
+private import semmle.python.dataflow.new.RemoteFlowSources
+private import semmle.python.dataflow.new.BarrierGuards
+
+/**
+ * Provides default sources, sinks and sanitizers for detecting
+ * "template injection"
+ * vulnerabilities, as well as extension points for adding your own.
+ */
+module TemplateInjection {
+ /**
+ * A data flow source for "template injection" vulnerabilities.
+ */
+ abstract class Source extends DataFlow::Node { }
+
+ /**
+ * A data flow sink for "template injection" vulnerabilities.
+ */
+ abstract class Sink extends DataFlow::Node { }
+
+ /**
+ * A sanitizer for "template injection" vulnerabilities.
+ */
+ abstract class Sanitizer extends DataFlow::Node { }
+
+ /**
+ * An active threat-model source, considered as a flow source.
+ */
+ private class ActiveThreatModelSourceAsSource extends Source, ActiveThreatModelSource { }
+
+ /**
+ * A SQL statement of a SQL construction, considered as a flow sink.
+ */
+ class TemplateConstructionAsSink extends Sink {
+ TemplateConstructionAsSink() { this = any(TemplateConstruction c).getSourceArg() }
+ }
+
+ /**
+ * A comparison with a constant, considered as a sanitizer-guard.
+ */
+ class ConstCompareAsSanitizerGuard extends Sanitizer, ConstCompareBarrier { }
+}
diff --git a/python/ql/lib/semmle/python/security/dataflow/TemplateInjectionQuery.qll b/python/ql/lib/semmle/python/security/dataflow/TemplateInjectionQuery.qll
new file mode 100644
index 000000000000..e5ad529fb37a
--- /dev/null
+++ b/python/ql/lib/semmle/python/security/dataflow/TemplateInjectionQuery.qll
@@ -0,0 +1,22 @@
+/**
+ * Provides a taint-tracking configuration for detecting "template injection" vulnerabilities.
+ *
+ * Note, for performance reasons: only import this file if
+ * `TemplateInjectionFlow` is needed, otherwise
+ * `TemplateInjectionCustomizations` should be imported instead.
+ */
+
+private import python
+import semmle.python.dataflow.new.DataFlow
+import semmle.python.dataflow.new.TaintTracking
+import TemplateInjectionCustomizations::TemplateInjection
+
+module TemplateInjectionConfig implements DataFlow::ConfigSig {
+ predicate isSource(DataFlow::Node node) { node instanceof Source }
+
+ predicate isSink(DataFlow::Node node) { node instanceof Sink }
+
+ predicate isBarrierIn(DataFlow::Node node) { node instanceof Sanitizer }
+}
+
+module TemplateInjectionFlow = TaintTracking::Global;
diff --git a/python/ql/src/experimental/Security/CWE-074/TemplateConstructionConcept.qll b/python/ql/src/experimental/Security/CWE-074/TemplateConstructionConcept.qll
index a20babf15eb6..b4f5cae44491 100644
--- a/python/ql/src/experimental/Security/CWE-074/TemplateConstructionConcept.qll
+++ b/python/ql/src/experimental/Security/CWE-074/TemplateConstructionConcept.qll
@@ -134,7 +134,12 @@ class Jinja2TemplateConstruction extends TemplateConstruction::Range, API::CallN
/** A call to `jinja2.from_string`. */
class Jinja2FromStringConstruction extends TemplateConstruction::Range, API::CallNode {
Jinja2FromStringConstruction() {
- this = API::moduleImport("jinja2").getMember("from_string").getACall()
+ this =
+ API::moduleImport("jinja2")
+ .getMember("Environment")
+ .getReturn()
+ .getMember("from_string")
+ .getACall()
}
override DataFlow::Node getSourceArg() { result = this.getArg(0) }
diff --git a/python/ql/src/experimental/Security/CWE-074/TemplateInjectionCustomizations.qll b/python/ql/src/experimental/Security/CWE-074/TemplateInjectionCustomizations.qll
index 593ca9fee4cf..13c70fc7d04d 100644
--- a/python/ql/src/experimental/Security/CWE-074/TemplateInjectionCustomizations.qll
+++ b/python/ql/src/experimental/Security/CWE-074/TemplateInjectionCustomizations.qll
@@ -6,7 +6,7 @@
private import python
private import semmle.python.dataflow.new.DataFlow
-private import semmle.python.Concepts
+private import semmle.python.Concepts as C
private import semmle.python.dataflow.new.RemoteFlowSources
private import semmle.python.dataflow.new.BarrierGuards
private import TemplateConstructionConcept
@@ -40,7 +40,7 @@ module TemplateInjection {
/**
* An active threat-model source, considered as a flow source.
*/
- private class ActiveThreatModelSourceAsSource extends Source, ActiveThreatModelSource { }
+ private class ActiveThreatModelSourceAsSource extends Source, C::ActiveThreatModelSource { }
/**
* A SQL statement of a SQL construction, considered as a flow sink.
From 60d8a85a9ccbd5c8099dd925dad7e98057c66c5b Mon Sep 17 00:00:00 2001
From: Joe Farebrother
Date: Wed, 6 Nov 2024 09:09:42 +0000
Subject: [PATCH 02/20] Promote jinja sinks
---
.../lib/semmle/python/frameworks/Jinja2.qll | 49 +++++++++++++++++++
.../CWE-074/TemplateConstructionConcept.qll | 1 +
2 files changed, 50 insertions(+)
diff --git a/python/ql/lib/semmle/python/frameworks/Jinja2.qll b/python/ql/lib/semmle/python/frameworks/Jinja2.qll
index e69de29bb2d1..0b681820f13c 100644
--- a/python/ql/lib/semmle/python/frameworks/Jinja2.qll
+++ b/python/ql/lib/semmle/python/frameworks/Jinja2.qll
@@ -0,0 +1,49 @@
+/**
+ * Provides classes modeling security-relevant aspects of the `jinja2` PyPI package.
+ * See https://jinja.palletsprojects.com.
+ */
+
+private import python
+private import semmle.python.dataflow.new.DataFlow
+private import semmle.python.ApiGraphs
+private import semmle.python.Concepts
+private import semmle.python.frameworks.data.ModelsAsData
+
+module Jinja2 {
+ /** A call to `jinja2.Template`. */
+ class Jinja2TemplateConstruction extends TemplateConstruction::Range, API::CallNode {
+ Jinja2TemplateConstruction() {
+ this = API::moduleImport("jinja2").getMember("Template").getACall()
+ }
+
+ override DataFlow::Node getSourceArg() { result = this.getArg(0) }
+ }
+
+ module EnvironmentClass {
+ /** Gets a reference to the `jinja2.Environment` class. */
+ API::Node classRef() {
+ result = API::moduleImport("jinja2").getMember("Environment")
+ or
+ result = ModelOutput::getATypeNode("jinja.Environment~Subclass").getASubclass*()
+ }
+
+ /** Gets a reference to an instance of `jinja2.Environment`. */
+ private DataFlow::TypeTrackingNode instance(DataFlow::TypeTracker t) {
+ t.start() and
+ result = EnvironmentClass::classRef().getACall()
+ or
+ exists(DataFlow::TypeTracker t2 | result = instance(t2).track(t2, t))
+ }
+
+ /** Gets a reference to an instance of `jinja2.Environment`. */
+ DataFlow::Node instance() { instance(DataFlow::TypeTracker::end()).flowsTo(result) }
+
+ /** A call to `jinja2.Environment.from_string`. */
+ class Jinja2FromStringConstruction extends TemplateConstruction::Range, DataFlow::MethodCallNode
+ {
+ Jinja2FromStringConstruction() { this.calls(EnvironmentClass::instance(), "from_string") }
+
+ override DataFlow::Node getSourceArg() { result = this.getArg(0) }
+ }
+ }
+}
diff --git a/python/ql/src/experimental/Security/CWE-074/TemplateConstructionConcept.qll b/python/ql/src/experimental/Security/CWE-074/TemplateConstructionConcept.qll
index b4f5cae44491..5144e2ff97b1 100644
--- a/python/ql/src/experimental/Security/CWE-074/TemplateConstructionConcept.qll
+++ b/python/ql/src/experimental/Security/CWE-074/TemplateConstructionConcept.qll
@@ -122,6 +122,7 @@ class GenshiMarkupTemplateConstruction extends TemplateConstruction::Range, API:
override DataFlow::Node getSourceArg() { result = this.getArg(0) }
}
+//
/** A call to `jinja2.Template`. */
class Jinja2TemplateConstruction extends TemplateConstruction::Range, API::CallNode {
Jinja2TemplateConstruction() {
From b2c13fe351894553b1e367c497a57ee10b5f3dc2 Mon Sep 17 00:00:00 2001
From: Joe Farebrother
Date: Wed, 6 Nov 2024 16:13:36 +0000
Subject: [PATCH 03/20] Promote template injection sinks for each framework
covered `Cheetah` was excluded as it was last updated 15 years ago and its
documentation links are dead.
---
python/ql/lib/semmle/python/Frameworks.qll | 8 ++++
.../lib/semmle/python/frameworks/Airspeed.qll | 26 +++++++++++
.../lib/semmle/python/frameworks/Bottle.qll | 14 +++++-
.../semmle/python/frameworks/Chameleon.qll | 26 +++++++++++
.../lib/semmle/python/frameworks/Chevron.qll | 26 +++++++++++
.../lib/semmle/python/frameworks/Django.qll | 15 +++++++
.../ql/lib/semmle/python/frameworks/Flask.qll | 9 ++++
.../lib/semmle/python/frameworks/Genshi.qll | 45 +++++++++++++++++++
.../lib/semmle/python/frameworks/Jinja2.qll | 11 ++++-
.../ql/lib/semmle/python/frameworks/Mako.qll | 26 +++++++++++
.../lib/semmle/python/frameworks/TRender.qll | 26 +++++++++++
11 files changed, 229 insertions(+), 3 deletions(-)
create mode 100644 python/ql/lib/semmle/python/frameworks/Airspeed.qll
create mode 100644 python/ql/lib/semmle/python/frameworks/Chameleon.qll
create mode 100644 python/ql/lib/semmle/python/frameworks/Chevron.qll
create mode 100644 python/ql/lib/semmle/python/frameworks/Genshi.qll
create mode 100644 python/ql/lib/semmle/python/frameworks/Mako.qll
create mode 100644 python/ql/lib/semmle/python/frameworks/TRender.qll
diff --git a/python/ql/lib/semmle/python/Frameworks.qll b/python/ql/lib/semmle/python/Frameworks.qll
index da35994b955d..af9417308ab3 100644
--- a/python/ql/lib/semmle/python/Frameworks.qll
+++ b/python/ql/lib/semmle/python/Frameworks.qll
@@ -11,13 +11,17 @@ private import semmle.python.frameworks.Aiohttp
private import semmle.python.frameworks.Aiomysql
private import semmle.python.frameworks.Aiopg
private import semmle.python.frameworks.Aiosqlite
+private import semmle.python.frameworks.Airspeed
private import semmle.python.frameworks.Anyio
private import semmle.python.frameworks.Asyncpg
private import semmle.python.frameworks.Baize
+private import semmle.python.frameworks.Bottle
private import semmle.python.frameworks.BSon
private import semmle.python.frameworks.Bottle
private import semmle.python.frameworks.CassandraDriver
+private import semmle.python.frameworks.Chameleon
private import semmle.python.frameworks.Cherrypy
+private import semmle.python.frameworks.Chevron
private import semmle.python.frameworks.ClickhouseDriver
private import semmle.python.frameworks.Cryptodome
private import semmle.python.frameworks.Cryptography
@@ -30,10 +34,12 @@ private import semmle.python.frameworks.FastApi
private import semmle.python.frameworks.Flask
private import semmle.python.frameworks.FlaskAdmin
private import semmle.python.frameworks.FlaskSqlAlchemy
+private import semmle.python.frameworks.Genshi
private import semmle.python.frameworks.Gradio
private import semmle.python.frameworks.Httpx
private import semmle.python.frameworks.Idna
private import semmle.python.frameworks.Invoke
+private import semmle.python.frameworks.Jinja2
private import semmle.python.frameworks.Jmespath
private import semmle.python.frameworks.Joblib
private import semmle.python.frameworks.JsonPickle
@@ -42,6 +48,7 @@ private import semmle.python.frameworks.Ldap3
private import semmle.python.frameworks.Libtaxii
private import semmle.python.frameworks.Libxml2
private import semmle.python.frameworks.Lxml
+private import semmle.python.frameworks.Mako
private import semmle.python.frameworks.MarkupSafe
private import semmle.python.frameworks.Multidict
private import semmle.python.frameworks.Mysql
@@ -78,6 +85,7 @@ private import semmle.python.frameworks.Streamlit
private import semmle.python.frameworks.Toml
private import semmle.python.frameworks.Torch
private import semmle.python.frameworks.Tornado
+private import semmle.python.frameworks.TRender
private import semmle.python.frameworks.Twisted
private import semmle.python.frameworks.Ujson
private import semmle.python.frameworks.Urllib3
diff --git a/python/ql/lib/semmle/python/frameworks/Airspeed.qll b/python/ql/lib/semmle/python/frameworks/Airspeed.qll
new file mode 100644
index 000000000000..bdfc2ae357db
--- /dev/null
+++ b/python/ql/lib/semmle/python/frameworks/Airspeed.qll
@@ -0,0 +1,26 @@
+/**
+ * Provides classes modeling security-relevant aspects of the `airspeed` library.
+ * See https://github.com/purcell/airspeed.
+ */
+
+private import python
+private import semmle.python.dataflow.new.DataFlow
+private import semmle.python.ApiGraphs
+private import semmle.python.Concepts
+
+/**
+ * INTERNAL: Do not use.
+ *
+ * Provides classes modeling security-relevant aspects of the `airspeed` library.
+ * See https://github.com/purcell/airspeed.
+ */
+module Airspeed {
+ /** A call to `airspeed.Template`. */
+ private class AirspeedTemplateConstruction extends TemplateConstruction::Range, API::CallNode {
+ AirspeedTemplateConstruction() {
+ this = API::moduleImport("airspeed").getMember("Template").getACall()
+ }
+
+ override DataFlow::Node getSourceArg() { result = this.getArg(0) }
+ }
+}
diff --git a/python/ql/lib/semmle/python/frameworks/Bottle.qll b/python/ql/lib/semmle/python/frameworks/Bottle.qll
index ce2a41dbaf4e..c03ea3df184e 100644
--- a/python/ql/lib/semmle/python/frameworks/Bottle.qll
+++ b/python/ql/lib/semmle/python/frameworks/Bottle.qll
@@ -39,7 +39,7 @@ module Bottle {
ViewCallable() { this = any(BottleRouteSetup rs).getARequestHandler() }
}
- /** Get methods that reprsent a route in Bottle */
+ /** Get methods that represent a route in Bottle */
string routeMethods() { result = ["route", "get", "post", "put", "delete", "patch"] }
private class BottleRouteSetup extends Http::Server::RouteSetup::Range, DataFlow::CallCfgNode {
@@ -171,5 +171,17 @@ module Bottle {
override predicate valueAllowsNewline() { none() }
}
}
+
+ /** Provides models for functions that construct templates. */
+ module Templates {
+ /** A call to `bottle.template`or `bottle.SimpleTemplate`. */
+ private class BottleTemplateConstruction extends TemplateConstruction::Range, API::CallNode {
+ BottleTemplateConstruction() {
+ this = API::moduleImport("bottle").getMember(["template", "SimpleTemplate"]).getACall()
+ }
+
+ override DataFlow::Node getSourceArg() { result = this.getArg(0) }
+ }
+ }
}
}
diff --git a/python/ql/lib/semmle/python/frameworks/Chameleon.qll b/python/ql/lib/semmle/python/frameworks/Chameleon.qll
new file mode 100644
index 000000000000..2f86d784b962
--- /dev/null
+++ b/python/ql/lib/semmle/python/frameworks/Chameleon.qll
@@ -0,0 +1,26 @@
+/**
+ * Provides classes modeling security-relevant aspects of the `chameleon` PyPI package.
+ * See https://chameleon.readthedocs.io/en/latest/.
+ */
+
+private import python
+private import semmle.python.dataflow.new.DataFlow
+private import semmle.python.ApiGraphs
+private import semmle.python.Concepts
+
+/**
+ * INTERNAL: Do not use.
+ *
+ * Provides classes modeling security-relevant aspects of the `chameleon` PyPI package.
+ * See https://chameleon.readthedocs.io/en/latest/.
+ */
+module Chameleon {
+ /** A call to `chameleon.PageTemplate`. */
+ private class ChameleonTemplateConstruction extends TemplateConstruction::Range, API::CallNode {
+ ChameleonTemplateConstruction() {
+ this = API::moduleImport("chameleon").getMember("PageTemplate").getACall()
+ }
+
+ override DataFlow::Node getSourceArg() { result = this.getArg(0) }
+ }
+}
diff --git a/python/ql/lib/semmle/python/frameworks/Chevron.qll b/python/ql/lib/semmle/python/frameworks/Chevron.qll
new file mode 100644
index 000000000000..5d938fef2086
--- /dev/null
+++ b/python/ql/lib/semmle/python/frameworks/Chevron.qll
@@ -0,0 +1,26 @@
+/**
+ * Provides classes modeling security-relevant aspects of the `chevron` PyPI package.
+ * See https://pypi.org/project/chevron.
+ */
+
+private import python
+private import semmle.python.dataflow.new.DataFlow
+private import semmle.python.ApiGraphs
+private import semmle.python.Concepts
+
+/**
+ * INTERNAL: Do not use.
+ *
+ * Provides classes modeling security-relevant aspects of the `chevron` PyPI package.
+ * See https://pypi.org/project/chevron.
+ */
+module Chevron {
+ /** A call to `chevron.render`. */
+ private class ChevronRenderConstruction extends TemplateConstruction::Range, API::CallNode {
+ ChevronRenderConstruction() {
+ this = API::moduleImport("chevron").getMember("render").getACall()
+ }
+
+ override DataFlow::Node getSourceArg() { result = this.getArg(0) }
+ }
+}
diff --git a/python/ql/lib/semmle/python/frameworks/Django.qll b/python/ql/lib/semmle/python/frameworks/Django.qll
index 351420818c38..80ef4aef435d 100644
--- a/python/ql/lib/semmle/python/frameworks/Django.qll
+++ b/python/ql/lib/semmle/python/frameworks/Django.qll
@@ -2996,4 +2996,19 @@ module PrivateDjango {
any()
}
}
+
+ // ---------------------------------------------------------------------------
+ // Templates
+ // ---------------------------------------------------------------------------
+
+ /** A call to `django.template.Template` */
+ private class DjangoTemplateConstruction extends TemplateConstruction::Range, API::CallNode {
+ DjangoTemplateConstruction() {
+ this = API::moduleImport("django").getMember("template").getMember("Template").getACall()
+ }
+
+ override DataFlow::Node getSourceArg() { result = this.getArg(0) }
+ }
+
+ // TODO: Support `from_string` on instances of `django.template.Engine`.
}
diff --git a/python/ql/lib/semmle/python/frameworks/Flask.qll b/python/ql/lib/semmle/python/frameworks/Flask.qll
index 62722a1958ac..cfb8048c6a13 100644
--- a/python/ql/lib/semmle/python/frameworks/Flask.qll
+++ b/python/ql/lib/semmle/python/frameworks/Flask.qll
@@ -721,4 +721,13 @@ module Flask {
preservesValue = false
}
}
+
+ /** A call to `flask.render_template_string` as a template construction sink. */
+ private class FlaskTemplateConstruction extends TemplateConstruction::Range, API::CallNode {
+ FlaskTemplateConstruction() {
+ this = API::moduleImport("flask").getMember("render_template_string").getACall()
+ }
+
+ override DataFlow::Node getSourceArg() { result = this.getArg(0) }
+ }
}
diff --git a/python/ql/lib/semmle/python/frameworks/Genshi.qll b/python/ql/lib/semmle/python/frameworks/Genshi.qll
new file mode 100644
index 000000000000..f01b5137aac3
--- /dev/null
+++ b/python/ql/lib/semmle/python/frameworks/Genshi.qll
@@ -0,0 +1,45 @@
+/**
+ * Provides classes modeling security-relevant aspects of the `Genshi` PyPI package.
+ * See https://genshi.edgewall.org/.
+ */
+
+private import python
+private import semmle.python.dataflow.new.DataFlow
+private import semmle.python.ApiGraphs
+private import semmle.python.Concepts
+
+/**
+ * INTERNAL: Do not use.
+ *
+ * Provides classes modeling security-relevant aspects of the `Genshi` PyPI package.
+ * See https://genshi.edgewall.org/.
+ */
+module Genshi {
+ /** A call to `genshi.template.text.NewTextTemplate` or `genshi.template.text.OldTextTemplate`. */
+ private class GenshiTextTemplateConstruction extends TemplateConstruction::Range, API::CallNode {
+ GenshiTextTemplateConstruction() {
+ this =
+ API::moduleImport("genshi")
+ .getMember("template")
+ .getMember("text")
+ .getMember(["NewTextTemplate", "OldTextTemplate"])
+ .getACall()
+ }
+
+ override DataFlow::Node getSourceArg() { result = this.getArg(0) }
+ }
+
+ /** A call to `genshi.template.MarkupTemplate` */
+ private class GenshiMarkupTemplateConstruction extends TemplateConstruction::Range, API::CallNode {
+ GenshiMarkupTemplateConstruction() {
+ this =
+ API::moduleImport("genshi")
+ .getMember("template")
+ .getMember("markup")
+ .getMember("MarkupTemplate")
+ .getACall()
+ }
+
+ override DataFlow::Node getSourceArg() { result = this.getArg(0) }
+ }
+}
diff --git a/python/ql/lib/semmle/python/frameworks/Jinja2.qll b/python/ql/lib/semmle/python/frameworks/Jinja2.qll
index 0b681820f13c..c89ffbe3cc97 100644
--- a/python/ql/lib/semmle/python/frameworks/Jinja2.qll
+++ b/python/ql/lib/semmle/python/frameworks/Jinja2.qll
@@ -9,9 +9,15 @@ private import semmle.python.ApiGraphs
private import semmle.python.Concepts
private import semmle.python.frameworks.data.ModelsAsData
+/**
+ * INTERNAL: Do not use
+ *
+ * Provides classes modeling security-relevant aspects of the `jinja2` PyPI package.
+ * See https://jinja.palletsprojects.com.
+ */
module Jinja2 {
/** A call to `jinja2.Template`. */
- class Jinja2TemplateConstruction extends TemplateConstruction::Range, API::CallNode {
+ private class Jinja2TemplateConstruction extends TemplateConstruction::Range, API::CallNode {
Jinja2TemplateConstruction() {
this = API::moduleImport("jinja2").getMember("Template").getACall()
}
@@ -39,7 +45,8 @@ module Jinja2 {
DataFlow::Node instance() { instance(DataFlow::TypeTracker::end()).flowsTo(result) }
/** A call to `jinja2.Environment.from_string`. */
- class Jinja2FromStringConstruction extends TemplateConstruction::Range, DataFlow::MethodCallNode
+ private class Jinja2FromStringConstruction extends TemplateConstruction::Range,
+ DataFlow::MethodCallNode
{
Jinja2FromStringConstruction() { this.calls(EnvironmentClass::instance(), "from_string") }
diff --git a/python/ql/lib/semmle/python/frameworks/Mako.qll b/python/ql/lib/semmle/python/frameworks/Mako.qll
new file mode 100644
index 000000000000..5dd518a8afe0
--- /dev/null
+++ b/python/ql/lib/semmle/python/frameworks/Mako.qll
@@ -0,0 +1,26 @@
+/**
+ * Provides classes modeling security-relevant aspects of the `Mako` PyPI package.
+ * See https://www.makotemplates.org/.
+ */
+
+private import python
+private import semmle.python.dataflow.new.DataFlow
+private import semmle.python.ApiGraphs
+private import semmle.python.Concepts
+
+/**
+ * INTERNAL: Do not use.
+ *
+ * Provides classes modeling security-relevant aspects of the `Mako` PyPI package.
+ * See https://www.makotemplates.org/.
+ */
+module Mako {
+ /** A call to `mako.template.Template`. */
+ private class MakoTemplateConstruction extends TemplateConstruction::Range, API::CallNode {
+ MakoTemplateConstruction() {
+ this = API::moduleImport("mako").getMember("template").getMember("Template").getACall()
+ }
+
+ override DataFlow::Node getSourceArg() { result = this.getArg(0) }
+ }
+}
diff --git a/python/ql/lib/semmle/python/frameworks/TRender.qll b/python/ql/lib/semmle/python/frameworks/TRender.qll
new file mode 100644
index 000000000000..08749676a06c
--- /dev/null
+++ b/python/ql/lib/semmle/python/frameworks/TRender.qll
@@ -0,0 +1,26 @@
+/**
+ * Provides classes modeling security-relevant aspects of the `trender` PyPI package.
+ * See https://github.com/cesbit/trender.
+ */
+
+private import python
+private import semmle.python.dataflow.new.DataFlow
+private import semmle.python.ApiGraphs
+private import semmle.python.Concepts
+
+/**
+ * INTERNAL: Do not use.
+ *
+ * Provides classes modeling security-relevant aspects of the `trender` PyPI package.
+ * See https://github.com/cesbit/trender.
+ */
+module TRender {
+ /** A call to `trender.TRender`. */
+ private class TRenderTemplateConstruction extends TemplateConstruction::Range, API::CallNode {
+ TRenderTemplateConstruction() {
+ this = API::moduleImport("trender").getMember("TRender").getACall()
+ }
+
+ override DataFlow::Node getSourceArg() { result = this.getArg(0) }
+ }
+}
From 71ab82dee06a4252c64e650c76773133cfc8d9e1 Mon Sep 17 00:00:00 2001
From: Joe Farebrother
Date: Thu, 7 Nov 2024 14:40:51 +0000
Subject: [PATCH 04/20] Fix qldoc, formatting, and redundant import warnings
---
python/ql/lib/semmle/python/Concepts.qll | 3 +--
python/ql/lib/semmle/python/frameworks/Airspeed.qll | 1 -
python/ql/lib/semmle/python/frameworks/Chameleon.qll | 1 -
python/ql/lib/semmle/python/frameworks/Chevron.qll | 1 -
python/ql/lib/semmle/python/frameworks/Django.qll | 2 --
python/ql/lib/semmle/python/frameworks/Genshi.qll | 1 -
python/ql/lib/semmle/python/frameworks/Jinja2.qll | 1 -
python/ql/lib/semmle/python/frameworks/Mako.qll | 1 -
python/ql/lib/semmle/python/frameworks/TRender.qll | 1 -
.../semmle/python/security/dataflow/TemplateInjectionQuery.qll | 3 ++-
10 files changed, 3 insertions(+), 12 deletions(-)
diff --git a/python/ql/lib/semmle/python/Concepts.qll b/python/ql/lib/semmle/python/Concepts.qll
index cf6f0214496f..94d660d75108 100644
--- a/python/ql/lib/semmle/python/Concepts.qll
+++ b/python/ql/lib/semmle/python/Concepts.qll
@@ -872,7 +872,7 @@ class TemplateConstruction extends DataFlow::Node instanceof TemplateConstructio
DataFlow::Node getSourceArg() { result = super.getSourceArg() }
}
-/** Provides classes for modelling template construction APIs. */
+/** Provides classes for modeling template construction APIs. */
module TemplateConstruction {
/**
* A data-flow node that constructs a template in a templating engine.
@@ -886,7 +886,6 @@ module TemplateConstruction {
}
}
-
/** Provides classes for modeling HTTP-related APIs. */
module Http {
/** Gets an HTTP verb, in upper case */
diff --git a/python/ql/lib/semmle/python/frameworks/Airspeed.qll b/python/ql/lib/semmle/python/frameworks/Airspeed.qll
index bdfc2ae357db..a08a1b4a46be 100644
--- a/python/ql/lib/semmle/python/frameworks/Airspeed.qll
+++ b/python/ql/lib/semmle/python/frameworks/Airspeed.qll
@@ -4,7 +4,6 @@
*/
private import python
-private import semmle.python.dataflow.new.DataFlow
private import semmle.python.ApiGraphs
private import semmle.python.Concepts
diff --git a/python/ql/lib/semmle/python/frameworks/Chameleon.qll b/python/ql/lib/semmle/python/frameworks/Chameleon.qll
index 2f86d784b962..cf5444c40ce2 100644
--- a/python/ql/lib/semmle/python/frameworks/Chameleon.qll
+++ b/python/ql/lib/semmle/python/frameworks/Chameleon.qll
@@ -4,7 +4,6 @@
*/
private import python
-private import semmle.python.dataflow.new.DataFlow
private import semmle.python.ApiGraphs
private import semmle.python.Concepts
diff --git a/python/ql/lib/semmle/python/frameworks/Chevron.qll b/python/ql/lib/semmle/python/frameworks/Chevron.qll
index 5d938fef2086..ec5676a2f04a 100644
--- a/python/ql/lib/semmle/python/frameworks/Chevron.qll
+++ b/python/ql/lib/semmle/python/frameworks/Chevron.qll
@@ -4,7 +4,6 @@
*/
private import python
-private import semmle.python.dataflow.new.DataFlow
private import semmle.python.ApiGraphs
private import semmle.python.Concepts
diff --git a/python/ql/lib/semmle/python/frameworks/Django.qll b/python/ql/lib/semmle/python/frameworks/Django.qll
index 80ef4aef435d..4aa5776ad54b 100644
--- a/python/ql/lib/semmle/python/frameworks/Django.qll
+++ b/python/ql/lib/semmle/python/frameworks/Django.qll
@@ -3000,7 +3000,6 @@ module PrivateDjango {
// ---------------------------------------------------------------------------
// Templates
// ---------------------------------------------------------------------------
-
/** A call to `django.template.Template` */
private class DjangoTemplateConstruction extends TemplateConstruction::Range, API::CallNode {
DjangoTemplateConstruction() {
@@ -3009,6 +3008,5 @@ module PrivateDjango {
override DataFlow::Node getSourceArg() { result = this.getArg(0) }
}
-
// TODO: Support `from_string` on instances of `django.template.Engine`.
}
diff --git a/python/ql/lib/semmle/python/frameworks/Genshi.qll b/python/ql/lib/semmle/python/frameworks/Genshi.qll
index f01b5137aac3..1e29295b4288 100644
--- a/python/ql/lib/semmle/python/frameworks/Genshi.qll
+++ b/python/ql/lib/semmle/python/frameworks/Genshi.qll
@@ -4,7 +4,6 @@
*/
private import python
-private import semmle.python.dataflow.new.DataFlow
private import semmle.python.ApiGraphs
private import semmle.python.Concepts
diff --git a/python/ql/lib/semmle/python/frameworks/Jinja2.qll b/python/ql/lib/semmle/python/frameworks/Jinja2.qll
index c89ffbe3cc97..9f267915e5c1 100644
--- a/python/ql/lib/semmle/python/frameworks/Jinja2.qll
+++ b/python/ql/lib/semmle/python/frameworks/Jinja2.qll
@@ -4,7 +4,6 @@
*/
private import python
-private import semmle.python.dataflow.new.DataFlow
private import semmle.python.ApiGraphs
private import semmle.python.Concepts
private import semmle.python.frameworks.data.ModelsAsData
diff --git a/python/ql/lib/semmle/python/frameworks/Mako.qll b/python/ql/lib/semmle/python/frameworks/Mako.qll
index 5dd518a8afe0..2209c0f89d2f 100644
--- a/python/ql/lib/semmle/python/frameworks/Mako.qll
+++ b/python/ql/lib/semmle/python/frameworks/Mako.qll
@@ -4,7 +4,6 @@
*/
private import python
-private import semmle.python.dataflow.new.DataFlow
private import semmle.python.ApiGraphs
private import semmle.python.Concepts
diff --git a/python/ql/lib/semmle/python/frameworks/TRender.qll b/python/ql/lib/semmle/python/frameworks/TRender.qll
index 08749676a06c..fae27f418c34 100644
--- a/python/ql/lib/semmle/python/frameworks/TRender.qll
+++ b/python/ql/lib/semmle/python/frameworks/TRender.qll
@@ -4,7 +4,6 @@
*/
private import python
-private import semmle.python.dataflow.new.DataFlow
private import semmle.python.ApiGraphs
private import semmle.python.Concepts
diff --git a/python/ql/lib/semmle/python/security/dataflow/TemplateInjectionQuery.qll b/python/ql/lib/semmle/python/security/dataflow/TemplateInjectionQuery.qll
index e5ad529fb37a..22c228f48d59 100644
--- a/python/ql/lib/semmle/python/security/dataflow/TemplateInjectionQuery.qll
+++ b/python/ql/lib/semmle/python/security/dataflow/TemplateInjectionQuery.qll
@@ -11,7 +11,7 @@ import semmle.python.dataflow.new.DataFlow
import semmle.python.dataflow.new.TaintTracking
import TemplateInjectionCustomizations::TemplateInjection
-module TemplateInjectionConfig implements DataFlow::ConfigSig {
+private module TemplateInjectionConfig implements DataFlow::ConfigSig {
predicate isSource(DataFlow::Node node) { node instanceof Source }
predicate isSink(DataFlow::Node node) { node instanceof Sink }
@@ -19,4 +19,5 @@ module TemplateInjectionConfig implements DataFlow::ConfigSig {
predicate isBarrierIn(DataFlow::Node node) { node instanceof Sanitizer }
}
+/** Global taint-tracking for detecting "template injection" vulnerabilities. */
module TemplateInjectionFlow = TaintTracking::Global;
From 1cb01a286d622d30a95077567fa42f02cd125669 Mon Sep 17 00:00:00 2001
From: Joe Farebrother
Date: Wed, 20 Nov 2024 14:50:35 +0000
Subject: [PATCH 05/20] Add tests for jinja
---
.../src/Security/CWE-074/TemplateInjection.ql | 19 ++++++++++++
.../CWE-074-TemplateInjection/JinjaSsti.py | 31 +++++++++++++++++++
.../TemplateInjection.expected | 16 ++++++++++
.../TemplateInjection.qlref | 1 +
4 files changed, 67 insertions(+)
create mode 100644 python/ql/src/Security/CWE-074/TemplateInjection.ql
create mode 100644 python/ql/test/query-tests/Security/CWE-074-TemplateInjection/JinjaSsti.py
create mode 100644 python/ql/test/query-tests/Security/CWE-074-TemplateInjection/TemplateInjection.expected
create mode 100644 python/ql/test/query-tests/Security/CWE-074-TemplateInjection/TemplateInjection.qlref
diff --git a/python/ql/src/Security/CWE-074/TemplateInjection.ql b/python/ql/src/Security/CWE-074/TemplateInjection.ql
new file mode 100644
index 000000000000..125478c801c8
--- /dev/null
+++ b/python/ql/src/Security/CWE-074/TemplateInjection.ql
@@ -0,0 +1,19 @@
+/**
+ * @name Server Side Template Injection
+ * @description Using user-controlled data to create a template can lead to remote code execution or cross site scripting.
+ * @kind path-problem
+ * @problem.severity error
+ * @precision high
+ * @id py/template-injection
+ * @tags security
+ * external/cwe/cwe-074
+ */
+
+import python
+import semmle.python.security.dataflow.TemplateInjectionQuery
+import TemplateInjectionFlow::PathGraph
+
+from TemplateInjectionFlow::PathNode source, TemplateInjectionFlow::PathNode sink
+where TemplateInjectionFlow::flowPath(source, sink)
+select sink.getNode(), source, sink, "This Template construction depends on $@.", source.getNode(),
+ "user-provided value"
diff --git a/python/ql/test/query-tests/Security/CWE-074-TemplateInjection/JinjaSsti.py b/python/ql/test/query-tests/Security/CWE-074-TemplateInjection/JinjaSsti.py
new file mode 100644
index 000000000000..f1fe834e4936
--- /dev/null
+++ b/python/ql/test/query-tests/Security/CWE-074-TemplateInjection/JinjaSsti.py
@@ -0,0 +1,31 @@
+from django.urls import path
+from django.http import HttpResponse
+from jinja2 import Template
+from jinja2 import Environment, DictLoader, escape
+
+
+def a(request):
+ # Load the template
+ template = request.GET['template']
+ t = Template(template) # BAD: Template constructed from user input
+ name = request.GET['name']
+ # Render the template with the context data
+ html = t.render(name=escape(name))
+ return HttpResponse(html)
+
+def b(request):
+ import jinja2
+ # Load the template
+ template = request.GET['template']
+ env = Environment()
+ t = env.from_string(template) # BAD: Template constructed from user input
+ name = request.GET['name']
+ # Render the template with the context data
+ html = t.render(name=escape(name))
+ return HttpResponse(html)
+
+
+urlpatterns = [
+ path('a', a),
+ path('b', b)
+]
diff --git a/python/ql/test/query-tests/Security/CWE-074-TemplateInjection/TemplateInjection.expected b/python/ql/test/query-tests/Security/CWE-074-TemplateInjection/TemplateInjection.expected
new file mode 100644
index 000000000000..3a833787a982
--- /dev/null
+++ b/python/ql/test/query-tests/Security/CWE-074-TemplateInjection/TemplateInjection.expected
@@ -0,0 +1,16 @@
+edges
+| JinjaSsti.py:7:7:7:13 | ControlFlowNode for request | JinjaSsti.py:9:5:9:12 | ControlFlowNode for template | provenance | AdditionalTaintStep |
+| JinjaSsti.py:9:5:9:12 | ControlFlowNode for template | JinjaSsti.py:10:18:10:25 | ControlFlowNode for template | provenance | |
+| JinjaSsti.py:16:7:16:13 | ControlFlowNode for request | JinjaSsti.py:19:5:19:12 | ControlFlowNode for template | provenance | AdditionalTaintStep |
+| JinjaSsti.py:19:5:19:12 | ControlFlowNode for template | JinjaSsti.py:21:25:21:32 | ControlFlowNode for template | provenance | |
+nodes
+| JinjaSsti.py:7:7:7:13 | ControlFlowNode for request | semmle.label | ControlFlowNode for request |
+| JinjaSsti.py:9:5:9:12 | ControlFlowNode for template | semmle.label | ControlFlowNode for template |
+| JinjaSsti.py:10:18:10:25 | ControlFlowNode for template | semmle.label | ControlFlowNode for template |
+| JinjaSsti.py:16:7:16:13 | ControlFlowNode for request | semmle.label | ControlFlowNode for request |
+| JinjaSsti.py:19:5:19:12 | ControlFlowNode for template | semmle.label | ControlFlowNode for template |
+| JinjaSsti.py:21:25:21:32 | ControlFlowNode for template | semmle.label | ControlFlowNode for template |
+subpaths
+#select
+| JinjaSsti.py:10:18:10:25 | ControlFlowNode for template | JinjaSsti.py:7:7:7:13 | ControlFlowNode for request | JinjaSsti.py:10:18:10:25 | ControlFlowNode for template | This Template construction depends on $@. | JinjaSsti.py:7:7:7:13 | ControlFlowNode for request | user-provided value |
+| JinjaSsti.py:21:25:21:32 | ControlFlowNode for template | JinjaSsti.py:16:7:16:13 | ControlFlowNode for request | JinjaSsti.py:21:25:21:32 | ControlFlowNode for template | This Template construction depends on $@. | JinjaSsti.py:16:7:16:13 | ControlFlowNode for request | user-provided value |
diff --git a/python/ql/test/query-tests/Security/CWE-074-TemplateInjection/TemplateInjection.qlref b/python/ql/test/query-tests/Security/CWE-074-TemplateInjection/TemplateInjection.qlref
new file mode 100644
index 000000000000..ead6bb469c6a
--- /dev/null
+++ b/python/ql/test/query-tests/Security/CWE-074-TemplateInjection/TemplateInjection.qlref
@@ -0,0 +1 @@
+Security/CWE-074/TemplateInjection.ql
\ No newline at end of file
From cea196ec61cec37fc47f526a88b4775a5a629d65 Mon Sep 17 00:00:00 2001
From: Joe Farebrother
Date: Thu, 21 Nov 2024 14:56:44 +0000
Subject: [PATCH 06/20] Add concepts tests + some fixes
---
.../ql/lib/semmle/python/frameworks/Flask.qll | 7 +++++--
.../ql/lib/semmle/python/frameworks/Genshi.qll | 2 +-
.../ql/lib/semmle/python/frameworks/Jinja2.qll | 1 +
.../ql/test/experimental/meta/ConceptsTest.qll | 17 ++++++++++++++++-
.../frameworks/Genshi/ConceptsTest.expected | 2 ++
.../frameworks/Genshi/ConceptsTest.ql | 2 ++
.../frameworks/Genshi/template_test.py | 9 +++++++++
.../frameworks/Mako/ConceptsTest.expected | 2 ++
.../frameworks/Mako/ConceptsTest.ql | 2 ++
.../frameworks/Mako/template_test.py | 4 ++++
.../frameworks/TRender/ConceptsTest.expected | 2 ++
.../frameworks/TRender/ConceptsTest.ql | 2 ++
.../frameworks/TRender/template_test.py | 4 ++++
.../frameworks/airspeed/ConceptsTest.expected | 2 ++
.../frameworks/airspeed/ConceptsTest.ql | 2 ++
.../frameworks/airspeed/template_test.py | 4 ++++
.../frameworks/bottle/template_test.py | 9 +++++++++
.../frameworks/chameleon/ConceptsTest.expected | 2 ++
.../frameworks/chameleon/ConceptsTest.ql | 2 ++
.../frameworks/chameleon/template_test.py | 4 ++++
.../frameworks/chevron/ConceptsTest.expected | 2 ++
.../frameworks/chevron/ConceptsTest.ql | 2 ++
.../frameworks/chevron/template_test.py | 4 ++++
.../frameworks/django-v2-v3/template_test.py | 17 +++++++++++++++++
.../frameworks/flask/taint_test.py | 8 ++++----
.../frameworks/flask/template_test.py | 16 ++++++++++++++++
.../frameworks/jinja2/ConceptsTest.expected | 2 ++
.../frameworks/jinja2/ConceptsTest.ql | 2 ++
.../frameworks/jinja2/template_test.py | 7 +++++++
29 files changed, 133 insertions(+), 8 deletions(-)
create mode 100644 python/ql/test/library-tests/frameworks/Genshi/ConceptsTest.expected
create mode 100644 python/ql/test/library-tests/frameworks/Genshi/ConceptsTest.ql
create mode 100644 python/ql/test/library-tests/frameworks/Genshi/template_test.py
create mode 100644 python/ql/test/library-tests/frameworks/Mako/ConceptsTest.expected
create mode 100644 python/ql/test/library-tests/frameworks/Mako/ConceptsTest.ql
create mode 100644 python/ql/test/library-tests/frameworks/Mako/template_test.py
create mode 100644 python/ql/test/library-tests/frameworks/TRender/ConceptsTest.expected
create mode 100644 python/ql/test/library-tests/frameworks/TRender/ConceptsTest.ql
create mode 100644 python/ql/test/library-tests/frameworks/TRender/template_test.py
create mode 100644 python/ql/test/library-tests/frameworks/airspeed/ConceptsTest.expected
create mode 100644 python/ql/test/library-tests/frameworks/airspeed/ConceptsTest.ql
create mode 100644 python/ql/test/library-tests/frameworks/airspeed/template_test.py
create mode 100644 python/ql/test/library-tests/frameworks/bottle/template_test.py
create mode 100644 python/ql/test/library-tests/frameworks/chameleon/ConceptsTest.expected
create mode 100644 python/ql/test/library-tests/frameworks/chameleon/ConceptsTest.ql
create mode 100644 python/ql/test/library-tests/frameworks/chameleon/template_test.py
create mode 100644 python/ql/test/library-tests/frameworks/chevron/ConceptsTest.expected
create mode 100644 python/ql/test/library-tests/frameworks/chevron/ConceptsTest.ql
create mode 100644 python/ql/test/library-tests/frameworks/chevron/template_test.py
create mode 100644 python/ql/test/library-tests/frameworks/django-v2-v3/template_test.py
create mode 100644 python/ql/test/library-tests/frameworks/flask/template_test.py
create mode 100644 python/ql/test/library-tests/frameworks/jinja2/ConceptsTest.expected
create mode 100644 python/ql/test/library-tests/frameworks/jinja2/ConceptsTest.ql
create mode 100644 python/ql/test/library-tests/frameworks/jinja2/template_test.py
diff --git a/python/ql/lib/semmle/python/frameworks/Flask.qll b/python/ql/lib/semmle/python/frameworks/Flask.qll
index cfb8048c6a13..0e5d6065c474 100644
--- a/python/ql/lib/semmle/python/frameworks/Flask.qll
+++ b/python/ql/lib/semmle/python/frameworks/Flask.qll
@@ -722,10 +722,13 @@ module Flask {
}
}
- /** A call to `flask.render_template_string` as a template construction sink. */
+ /** A call to `flask.render_template_string` or `flask.stream_template_string` as a template construction sink. */
private class FlaskTemplateConstruction extends TemplateConstruction::Range, API::CallNode {
FlaskTemplateConstruction() {
- this = API::moduleImport("flask").getMember("render_template_string").getACall()
+ this =
+ API::moduleImport("flask")
+ .getMember(["render_template_string", "stream_template_string"])
+ .getACall()
}
override DataFlow::Node getSourceArg() { result = this.getArg(0) }
diff --git a/python/ql/lib/semmle/python/frameworks/Genshi.qll b/python/ql/lib/semmle/python/frameworks/Genshi.qll
index 1e29295b4288..8e368391cf74 100644
--- a/python/ql/lib/semmle/python/frameworks/Genshi.qll
+++ b/python/ql/lib/semmle/python/frameworks/Genshi.qll
@@ -21,7 +21,7 @@ module Genshi {
API::moduleImport("genshi")
.getMember("template")
.getMember("text")
- .getMember(["NewTextTemplate", "OldTextTemplate"])
+ .getMember(["NewTextTemplate", "OldTextTemplate", "TextTemplate"])
.getACall()
}
diff --git a/python/ql/lib/semmle/python/frameworks/Jinja2.qll b/python/ql/lib/semmle/python/frameworks/Jinja2.qll
index 9f267915e5c1..0d0a8d989211 100644
--- a/python/ql/lib/semmle/python/frameworks/Jinja2.qll
+++ b/python/ql/lib/semmle/python/frameworks/Jinja2.qll
@@ -24,6 +24,7 @@ module Jinja2 {
override DataFlow::Node getSourceArg() { result = this.getArg(0) }
}
+ /** Definitions for modeling jinja `Environment`s. */
module EnvironmentClass {
/** Gets a reference to the `jinja2.Environment` class. */
API::Node classRef() {
diff --git a/python/ql/test/experimental/meta/ConceptsTest.qll b/python/ql/test/experimental/meta/ConceptsTest.qll
index 8ab87e56d1c4..40aa9c951b0d 100644
--- a/python/ql/test/experimental/meta/ConceptsTest.qll
+++ b/python/ql/test/experimental/meta/ConceptsTest.qll
@@ -663,6 +663,20 @@ module CorsMiddlewareTest implements TestSig {
}
}
+module TemplateConstructionTest implements TestSig {
+ string getARelevantTag() { result = "templateConstruction" }
+
+ predicate hasActualResult(Location location, string element, string tag, string value) {
+ exists(location.getFile().getRelativePath()) and
+ exists(TemplateConstruction tc |
+ location = tc.getLocation() and
+ element = tc.toString() and
+ value = prettyNodeForInlineTest(tc.getSourceArg()) and
+ tag = "templateConstruction"
+ )
+ }
+}
+
import MakeTest,
MergeTests5,
MergeTests5>>>
+ CsrfLocalProtectionSettingTest,
+ MergeTests3>>>
diff --git a/python/ql/test/library-tests/frameworks/Genshi/ConceptsTest.expected b/python/ql/test/library-tests/frameworks/Genshi/ConceptsTest.expected
new file mode 100644
index 000000000000..a74f2c23cda2
--- /dev/null
+++ b/python/ql/test/library-tests/frameworks/Genshi/ConceptsTest.expected
@@ -0,0 +1,2 @@
+testFailures
+failures
\ No newline at end of file
diff --git a/python/ql/test/library-tests/frameworks/Genshi/ConceptsTest.ql b/python/ql/test/library-tests/frameworks/Genshi/ConceptsTest.ql
new file mode 100644
index 000000000000..b557a0bccb69
--- /dev/null
+++ b/python/ql/test/library-tests/frameworks/Genshi/ConceptsTest.ql
@@ -0,0 +1,2 @@
+import python
+import experimental.meta.ConceptsTest
diff --git a/python/ql/test/library-tests/frameworks/Genshi/template_test.py b/python/ql/test/library-tests/frameworks/Genshi/template_test.py
new file mode 100644
index 000000000000..d585ee1a81ea
--- /dev/null
+++ b/python/ql/test/library-tests/frameworks/Genshi/template_test.py
@@ -0,0 +1,9 @@
+from genshi.template.text import TextTemplate, NewTextTemplate, OldTextTemplate
+from genshi.template.markup import MarkupTemplate
+
+def test():
+ a = TextTemplate("abc") # $ templateConstruction="abc"
+ a = OldTextTemplate("abc") # $ templateConstruction="abc"
+ a = NewTextTemplate("abc") # $ templateConstruction="abc"
+ a = MarkupTemplate("abc") # $ templateConstruction="abc"
+ return a
\ No newline at end of file
diff --git a/python/ql/test/library-tests/frameworks/Mako/ConceptsTest.expected b/python/ql/test/library-tests/frameworks/Mako/ConceptsTest.expected
new file mode 100644
index 000000000000..a74f2c23cda2
--- /dev/null
+++ b/python/ql/test/library-tests/frameworks/Mako/ConceptsTest.expected
@@ -0,0 +1,2 @@
+testFailures
+failures
\ No newline at end of file
diff --git a/python/ql/test/library-tests/frameworks/Mako/ConceptsTest.ql b/python/ql/test/library-tests/frameworks/Mako/ConceptsTest.ql
new file mode 100644
index 000000000000..b557a0bccb69
--- /dev/null
+++ b/python/ql/test/library-tests/frameworks/Mako/ConceptsTest.ql
@@ -0,0 +1,2 @@
+import python
+import experimental.meta.ConceptsTest
diff --git a/python/ql/test/library-tests/frameworks/Mako/template_test.py b/python/ql/test/library-tests/frameworks/Mako/template_test.py
new file mode 100644
index 000000000000..224954cf2633
--- /dev/null
+++ b/python/ql/test/library-tests/frameworks/Mako/template_test.py
@@ -0,0 +1,4 @@
+from mako.template import Template
+
+def test():
+ return Template("abc") # $ templateConstruction="abc"
\ No newline at end of file
diff --git a/python/ql/test/library-tests/frameworks/TRender/ConceptsTest.expected b/python/ql/test/library-tests/frameworks/TRender/ConceptsTest.expected
new file mode 100644
index 000000000000..a74f2c23cda2
--- /dev/null
+++ b/python/ql/test/library-tests/frameworks/TRender/ConceptsTest.expected
@@ -0,0 +1,2 @@
+testFailures
+failures
\ No newline at end of file
diff --git a/python/ql/test/library-tests/frameworks/TRender/ConceptsTest.ql b/python/ql/test/library-tests/frameworks/TRender/ConceptsTest.ql
new file mode 100644
index 000000000000..b557a0bccb69
--- /dev/null
+++ b/python/ql/test/library-tests/frameworks/TRender/ConceptsTest.ql
@@ -0,0 +1,2 @@
+import python
+import experimental.meta.ConceptsTest
diff --git a/python/ql/test/library-tests/frameworks/TRender/template_test.py b/python/ql/test/library-tests/frameworks/TRender/template_test.py
new file mode 100644
index 000000000000..f62d33c26d53
--- /dev/null
+++ b/python/ql/test/library-tests/frameworks/TRender/template_test.py
@@ -0,0 +1,4 @@
+from trender import TRender
+
+def test():
+ return TRender("abc") # $ templateConstruction="abc"
\ No newline at end of file
diff --git a/python/ql/test/library-tests/frameworks/airspeed/ConceptsTest.expected b/python/ql/test/library-tests/frameworks/airspeed/ConceptsTest.expected
new file mode 100644
index 000000000000..a74f2c23cda2
--- /dev/null
+++ b/python/ql/test/library-tests/frameworks/airspeed/ConceptsTest.expected
@@ -0,0 +1,2 @@
+testFailures
+failures
\ No newline at end of file
diff --git a/python/ql/test/library-tests/frameworks/airspeed/ConceptsTest.ql b/python/ql/test/library-tests/frameworks/airspeed/ConceptsTest.ql
new file mode 100644
index 000000000000..b557a0bccb69
--- /dev/null
+++ b/python/ql/test/library-tests/frameworks/airspeed/ConceptsTest.ql
@@ -0,0 +1,2 @@
+import python
+import experimental.meta.ConceptsTest
diff --git a/python/ql/test/library-tests/frameworks/airspeed/template_test.py b/python/ql/test/library-tests/frameworks/airspeed/template_test.py
new file mode 100644
index 000000000000..34d4c29fff6f
--- /dev/null
+++ b/python/ql/test/library-tests/frameworks/airspeed/template_test.py
@@ -0,0 +1,4 @@
+from airspeed import Template
+
+def test():
+ return Template("abc") # $ templateConstruction="abc"
diff --git a/python/ql/test/library-tests/frameworks/bottle/template_test.py b/python/ql/test/library-tests/frameworks/bottle/template_test.py
new file mode 100644
index 000000000000..db48cfc4fc9f
--- /dev/null
+++ b/python/ql/test/library-tests/frameworks/bottle/template_test.py
@@ -0,0 +1,9 @@
+import bottle
+from bottle import response, request, template, SimpleTemplate
+
+app = bottle.app()
+@app.route('/test', method=['OPTIONS', 'GET']) # $ routeSetup="/test"
+def test1(): # $ requestHandler
+ template("abc") # $ templateConstruction="abc"
+ SimpleTemplate("abc") # $ templateConstruction="abc"
+ return '[1]' # $ HttpResponse mimetype=text/html responseBody='[1]'
\ No newline at end of file
diff --git a/python/ql/test/library-tests/frameworks/chameleon/ConceptsTest.expected b/python/ql/test/library-tests/frameworks/chameleon/ConceptsTest.expected
new file mode 100644
index 000000000000..a74f2c23cda2
--- /dev/null
+++ b/python/ql/test/library-tests/frameworks/chameleon/ConceptsTest.expected
@@ -0,0 +1,2 @@
+testFailures
+failures
\ No newline at end of file
diff --git a/python/ql/test/library-tests/frameworks/chameleon/ConceptsTest.ql b/python/ql/test/library-tests/frameworks/chameleon/ConceptsTest.ql
new file mode 100644
index 000000000000..b557a0bccb69
--- /dev/null
+++ b/python/ql/test/library-tests/frameworks/chameleon/ConceptsTest.ql
@@ -0,0 +1,2 @@
+import python
+import experimental.meta.ConceptsTest
diff --git a/python/ql/test/library-tests/frameworks/chameleon/template_test.py b/python/ql/test/library-tests/frameworks/chameleon/template_test.py
new file mode 100644
index 000000000000..ad6d85036eab
--- /dev/null
+++ b/python/ql/test/library-tests/frameworks/chameleon/template_test.py
@@ -0,0 +1,4 @@
+from chameleon import PageTemplate
+
+def test():
+ return PageTemplate("abc") # $ templateConstruction="abc"
\ No newline at end of file
diff --git a/python/ql/test/library-tests/frameworks/chevron/ConceptsTest.expected b/python/ql/test/library-tests/frameworks/chevron/ConceptsTest.expected
new file mode 100644
index 000000000000..a74f2c23cda2
--- /dev/null
+++ b/python/ql/test/library-tests/frameworks/chevron/ConceptsTest.expected
@@ -0,0 +1,2 @@
+testFailures
+failures
\ No newline at end of file
diff --git a/python/ql/test/library-tests/frameworks/chevron/ConceptsTest.ql b/python/ql/test/library-tests/frameworks/chevron/ConceptsTest.ql
new file mode 100644
index 000000000000..b557a0bccb69
--- /dev/null
+++ b/python/ql/test/library-tests/frameworks/chevron/ConceptsTest.ql
@@ -0,0 +1,2 @@
+import python
+import experimental.meta.ConceptsTest
diff --git a/python/ql/test/library-tests/frameworks/chevron/template_test.py b/python/ql/test/library-tests/frameworks/chevron/template_test.py
new file mode 100644
index 000000000000..7aff524166d5
--- /dev/null
+++ b/python/ql/test/library-tests/frameworks/chevron/template_test.py
@@ -0,0 +1,4 @@
+from chevron import render
+
+def test():
+ return render("abc") # $ templateConstruction="abc"
\ No newline at end of file
diff --git a/python/ql/test/library-tests/frameworks/django-v2-v3/template_test.py b/python/ql/test/library-tests/frameworks/django-v2-v3/template_test.py
new file mode 100644
index 000000000000..2d25848fde65
--- /dev/null
+++ b/python/ql/test/library-tests/frameworks/django-v2-v3/template_test.py
@@ -0,0 +1,17 @@
+from django.template import Template, engines
+from django.urls import path
+from django.http.response import HttpResponse,
+
+def a(request): # $requestHandler
+ t = Template("abc").render() # $templateConstruction="abc"
+ return HttpResponse(t) # $HttpResponse
+
+def b(request): # $requestHandler
+ # This case is not yet supported
+ t = django.template.engines["django"].from_string("abc") # $MISSING:templateConstruction="abc"
+ return HttpResponse(t) # $HttpResponse
+
+urlpatterns = [
+ path("a", a), # $ routeSetup="a"
+ path("b", b), # $ routeSetup="b"
+]
\ No newline at end of file
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 227aecbf7452..ac8a5a82dc28 100644
--- a/python/ql/test/library-tests/frameworks/flask/taint_test.py
+++ b/python/ql/test/library-tests/frameworks/flask/taint_test.py
@@ -222,25 +222,25 @@ def test_taint(name = "World!", number="0", foo="foo"): # $requestHandler route
# render_template_string
source = TAINTED_STRING
ensure_tainted(source) # $ tainted
- res = render_template_string(source)
+ res = render_template_string(source) # $ templateConstruction=source
ensure_tainted(res) # $ tainted
# since template variables are auto-escaped, we don't treat result as tainted
# see https://flask.palletsprojects.com/en/2.3.x/api/#flask.render_template_string
- res = render_template_string("Hello {{ foo }}", foo=TAINTED_STRING)
+ res = render_template_string("Hello {{ foo }}", foo=TAINTED_STRING) # $ templateConstruction="Hello {{ foo }}"
ensure_not_tainted(res)
# stream_template_string
source = TAINTED_STRING
ensure_tainted(source) # $ tainted
- res = stream_template_string(source)
+ res = stream_template_string(source) # $ templateConstruction=source
for x in res:
ensure_tainted(x) # $ tainted
# since template variables are auto-escaped, we don't treat result as tainted
# see https://flask.palletsprojects.com/en/2.3.x/api/#flask.stream_template_string
- res = stream_template_string("Hello {{ foo }}", foo=TAINTED_STRING)
+ res = stream_template_string("Hello {{ foo }}", foo=TAINTED_STRING) # $ templateConstruction="Hello {{ foo }}"
for x in res:
ensure_not_tainted(x)
diff --git a/python/ql/test/library-tests/frameworks/flask/template_test.py b/python/ql/test/library-tests/frameworks/flask/template_test.py
new file mode 100644
index 000000000000..8d867e148291
--- /dev/null
+++ b/python/ql/test/library-tests/frameworks/flask/template_test.py
@@ -0,0 +1,16 @@
+from flask import Flask, Response, stream_with_context, render_template_string, stream_template_string
+app = Flask(__name__)
+
+@app.route("/a") # $routeSetup="/a"
+def a(): # $requestHandler
+ r = render_template_string("abc") # $ templateConstruction="abc"
+ return r # $ HttpResponse
+
+@app.route("/b") # $routeSetup="/b"
+def b(): # $requestHandler
+ s = stream_template_string("abc") # $ templateConstruction="abc"
+ r = Response(stream_with_context(s)) # $ HttpResponse
+ return r # $ HttpResponse
+
+if __name__ == "__main__":
+ app.run(debug=True)
\ No newline at end of file
diff --git a/python/ql/test/library-tests/frameworks/jinja2/ConceptsTest.expected b/python/ql/test/library-tests/frameworks/jinja2/ConceptsTest.expected
new file mode 100644
index 000000000000..a74f2c23cda2
--- /dev/null
+++ b/python/ql/test/library-tests/frameworks/jinja2/ConceptsTest.expected
@@ -0,0 +1,2 @@
+testFailures
+failures
\ No newline at end of file
diff --git a/python/ql/test/library-tests/frameworks/jinja2/ConceptsTest.ql b/python/ql/test/library-tests/frameworks/jinja2/ConceptsTest.ql
new file mode 100644
index 000000000000..b557a0bccb69
--- /dev/null
+++ b/python/ql/test/library-tests/frameworks/jinja2/ConceptsTest.ql
@@ -0,0 +1,2 @@
+import python
+import experimental.meta.ConceptsTest
diff --git a/python/ql/test/library-tests/frameworks/jinja2/template_test.py b/python/ql/test/library-tests/frameworks/jinja2/template_test.py
new file mode 100644
index 000000000000..587de84f6216
--- /dev/null
+++ b/python/ql/test/library-tests/frameworks/jinja2/template_test.py
@@ -0,0 +1,7 @@
+from jinja2 import Environment, Template
+
+def test():
+ env = Environment()
+ t = env.from_string("abc") # $ templateConstruction="abc"
+ t = Template("abc") # $ templateConstruction="abc"
+ return t
\ No newline at end of file
From 02f395f5f845e5c11aed8ff988867bd8e9722060 Mon Sep 17 00:00:00 2001
From: Joe Farebrother
Date: Thu, 21 Nov 2024 16:51:28 +0000
Subject: [PATCH 07/20] Add qhelp
---
.../Security/CWE-074/TemplateInjection.qhelp | 30 +++++++++++++++++++
.../src/Security/CWE-074/examples/JinjaBad.py | 19 ++++++++++++
.../CWE-074/examples/JinjaGoodParam.py | 17 +++++++++++
.../CWE-074/examples/JinjaGoodSandbox.py | 21 +++++++++++++
.../CWE-074/examples/template_exploit.txt | 1 +
5 files changed, 88 insertions(+)
create mode 100644 python/ql/src/Security/CWE-074/TemplateInjection.qhelp
create mode 100644 python/ql/src/Security/CWE-074/examples/JinjaBad.py
create mode 100644 python/ql/src/Security/CWE-074/examples/JinjaGoodParam.py
create mode 100644 python/ql/src/Security/CWE-074/examples/JinjaGoodSandbox.py
create mode 100644 python/ql/src/Security/CWE-074/examples/template_exploit.txt
diff --git a/python/ql/src/Security/CWE-074/TemplateInjection.qhelp b/python/ql/src/Security/CWE-074/TemplateInjection.qhelp
new file mode 100644
index 000000000000..06990a7237b6
--- /dev/null
+++ b/python/ql/src/Security/CWE-074/TemplateInjection.qhelp
@@ -0,0 +1,30 @@
+
+
+
+
+ A template from a server templating engine such as Jinja constructed from user input can allow the user to execute arbitrary code using certain template features. It can also allow for cross-site scripting.
+
+
+
+
+ Ensure that an untrusted value is not used to directly construct a template.
+ Jinja also provides a SandboxedEnvironment that prohibits access to unsafe methods and attributes, that can be used if constructing a template from user input is absolutely necessary.
+
+
+
+ In the following case template is used to generate a Jinja2 template string. This can lead to remote code execution.
+
+
+ The following is an example of a string that could be used to cause remote code execution when interpreted as a template:
+
+
+ In the following case, user input is not used to construct the template; rather is only used for as the parameters to render the template, which is safe.
+
+
+ In the following case, a SandboxedEnvironment is used, preventing remote code execution.
+
+
+
+ Portswigger : [Server Side Template Injection](https://portswigger.net/web-security/server-side-template-injection)
+
+
diff --git a/python/ql/src/Security/CWE-074/examples/JinjaBad.py b/python/ql/src/Security/CWE-074/examples/JinjaBad.py
new file mode 100644
index 000000000000..0a82135b49ba
--- /dev/null
+++ b/python/ql/src/Security/CWE-074/examples/JinjaBad.py
@@ -0,0 +1,19 @@
+from django.urls import path
+from django.http import HttpResponse
+from jinja2 import Template, escape
+
+
+def a(request):
+ template = request.GET['template']
+
+ # BAD: Template is constructed from user input.
+ t = Template(template)
+
+ name = request.GET['name']
+ html = t.render(name=escape(name))
+ return HttpResponse(html)
+
+
+urlpatterns = [
+ path('a', a),
+]
\ No newline at end of file
diff --git a/python/ql/src/Security/CWE-074/examples/JinjaGoodParam.py b/python/ql/src/Security/CWE-074/examples/JinjaGoodParam.py
new file mode 100644
index 000000000000..1d8bb6962f6a
--- /dev/null
+++ b/python/ql/src/Security/CWE-074/examples/JinjaGoodParam.py
@@ -0,0 +1,17 @@
+from django.urls import path
+from django.http import HttpResponse
+from jinja2 import Template, escape
+
+
+def a(request):
+ # GOOD: Template is a constant, not constructed from user input
+ t = Template("Hello, {{name}}!")
+
+ name = request.GET['name']
+ html = t.render(name=escape(name))
+ return HttpResponse(html)
+
+
+urlpatterns = [
+ path('a', a),
+]
\ No newline at end of file
diff --git a/python/ql/src/Security/CWE-074/examples/JinjaGoodSandbox.py b/python/ql/src/Security/CWE-074/examples/JinjaGoodSandbox.py
new file mode 100644
index 000000000000..488591c6f83e
--- /dev/null
+++ b/python/ql/src/Security/CWE-074/examples/JinjaGoodSandbox.py
@@ -0,0 +1,21 @@
+from django.urls import path
+from django.http import HttpResponse
+from jinja2 import escape
+from jinja2.sandbox import SandboxedEnvironment
+
+
+def a(request):
+ env = SandboxedEnvironment()
+ template = request.GET['template']
+
+ # GOOD: A sandboxed environment is used to construct the template.
+ t = env.from_string(template)
+
+ name = request.GET['name']
+ html = t.render(name=escape(name))
+ return HttpResponse(html)
+
+
+urlpatterns = [
+ path('a', a),
+]
\ No newline at end of file
diff --git a/python/ql/src/Security/CWE-074/examples/template_exploit.txt b/python/ql/src/Security/CWE-074/examples/template_exploit.txt
new file mode 100644
index 000000000000..607b95bd8d8e
--- /dev/null
+++ b/python/ql/src/Security/CWE-074/examples/template_exploit.txt
@@ -0,0 +1 @@
+{% for s in ().__class__.__base__.__subclasses__() %}{% if "warning" in s.__name__ %}{{s()._module.__builtins__['__import__']('os').system('cat /etc/passwd') }}{% endif %}{% endfor %}
From e4e02ec6749947b6f2805482d07ea7d13ac86060 Mon Sep 17 00:00:00 2001
From: Joe Farebrother
Date: Thu, 21 Nov 2024 16:59:12 +0000
Subject: [PATCH 08/20] Add security severity + fix qhelp
---
python/ql/src/Security/CWE-074/TemplateInjection.qhelp | 2 +-
python/ql/src/Security/CWE-074/TemplateInjection.ql | 1 +
2 files changed, 2 insertions(+), 1 deletion(-)
diff --git a/python/ql/src/Security/CWE-074/TemplateInjection.qhelp b/python/ql/src/Security/CWE-074/TemplateInjection.qhelp
index 06990a7237b6..477d1b0e139f 100644
--- a/python/ql/src/Security/CWE-074/TemplateInjection.qhelp
+++ b/python/ql/src/Security/CWE-074/TemplateInjection.qhelp
@@ -12,7 +12,7 @@
- In the following case template is used to generate a Jinja2 template string. This can lead to remote code execution.
+ In the following case, template is used to generate a Jinja2 template string. This can lead to remote code execution.
The following is an example of a string that could be used to cause remote code execution when interpreted as a template:
diff --git a/python/ql/src/Security/CWE-074/TemplateInjection.ql b/python/ql/src/Security/CWE-074/TemplateInjection.ql
index 125478c801c8..2ea68414259e 100644
--- a/python/ql/src/Security/CWE-074/TemplateInjection.ql
+++ b/python/ql/src/Security/CWE-074/TemplateInjection.ql
@@ -4,6 +4,7 @@
* @kind path-problem
* @problem.severity error
* @precision high
+ * @security-severity 9.3
* @id py/template-injection
* @tags security
* external/cwe/cwe-074
From 4602c5c90545c4d4b59464e4ccad51f0bd75a3da Mon Sep 17 00:00:00 2001
From: Joe Farebrother
Date: Thu, 21 Nov 2024 17:06:25 +0000
Subject: [PATCH 09/20] Remove experimental version + qhelp fixes
---
.../Security/CWE-074/TemplateInjection.qhelp | 4 +-
.../experimental/Security/CWE-074/JinjaBad.py | 19 --
.../Security/CWE-074/JinjaGood.py | 20 ---
.../CWE-074/TemplateConstructionConcept.qll | 165 ------------------
.../Security/CWE-074/TemplateInjection.qhelp | 24 ---
.../Security/CWE-074/TemplateInjection.ql | 20 ---
.../TemplateInjectionCustomizations.qll | 59 -------
.../CWE-074/TemplateInjectionQuery.qll | 18 --
.../CWE-074-TemplateInjection/AirspeedSsti.py | 11 --
.../CWE-074-TemplateInjection/BottleSsti.py | 20 ---
.../CWE-074-TemplateInjection/Chameleon.py | 10 --
.../CWE-074-TemplateInjection/CheetahSinks.py | 22 ---
.../CWE-074-TemplateInjection/ChevronSsti.py | 24 ---
.../DjangoTemplates.py | 41 -----
.../FlaskTemplate.py | 22 ---
.../CWE-074-TemplateInjection/Genshi.py | 18 --
.../CWE-074-TemplateInjection/JinjaSsti.py | 30 ----
.../CWE-074-TemplateInjection/MakoSsti.py | 15 --
.../CWE-074-TemplateInjection/TRender.py | 12 --
.../TemplateInjection.expected | 107 ------------
.../TemplateInjection.qlref | 1 -
21 files changed, 2 insertions(+), 660 deletions(-)
delete mode 100644 python/ql/src/experimental/Security/CWE-074/JinjaBad.py
delete mode 100644 python/ql/src/experimental/Security/CWE-074/JinjaGood.py
delete mode 100644 python/ql/src/experimental/Security/CWE-074/TemplateConstructionConcept.qll
delete mode 100644 python/ql/src/experimental/Security/CWE-074/TemplateInjection.qhelp
delete mode 100644 python/ql/src/experimental/Security/CWE-074/TemplateInjection.ql
delete mode 100644 python/ql/src/experimental/Security/CWE-074/TemplateInjectionCustomizations.qll
delete mode 100644 python/ql/src/experimental/Security/CWE-074/TemplateInjectionQuery.qll
delete mode 100644 python/ql/test/experimental/query-tests/Security/CWE-074-TemplateInjection/AirspeedSsti.py
delete mode 100644 python/ql/test/experimental/query-tests/Security/CWE-074-TemplateInjection/BottleSsti.py
delete mode 100644 python/ql/test/experimental/query-tests/Security/CWE-074-TemplateInjection/Chameleon.py
delete mode 100644 python/ql/test/experimental/query-tests/Security/CWE-074-TemplateInjection/CheetahSinks.py
delete mode 100644 python/ql/test/experimental/query-tests/Security/CWE-074-TemplateInjection/ChevronSsti.py
delete mode 100644 python/ql/test/experimental/query-tests/Security/CWE-074-TemplateInjection/DjangoTemplates.py
delete mode 100644 python/ql/test/experimental/query-tests/Security/CWE-074-TemplateInjection/FlaskTemplate.py
delete mode 100644 python/ql/test/experimental/query-tests/Security/CWE-074-TemplateInjection/Genshi.py
delete mode 100644 python/ql/test/experimental/query-tests/Security/CWE-074-TemplateInjection/JinjaSsti.py
delete mode 100644 python/ql/test/experimental/query-tests/Security/CWE-074-TemplateInjection/MakoSsti.py
delete mode 100644 python/ql/test/experimental/query-tests/Security/CWE-074-TemplateInjection/TRender.py
delete mode 100644 python/ql/test/experimental/query-tests/Security/CWE-074-TemplateInjection/TemplateInjection.expected
delete mode 100644 python/ql/test/experimental/query-tests/Security/CWE-074-TemplateInjection/TemplateInjection.qlref
diff --git a/python/ql/src/Security/CWE-074/TemplateInjection.qhelp b/python/ql/src/Security/CWE-074/TemplateInjection.qhelp
index 477d1b0e139f..619d834d1cfe 100644
--- a/python/ql/src/Security/CWE-074/TemplateInjection.qhelp
+++ b/python/ql/src/Security/CWE-074/TemplateInjection.qhelp
@@ -16,10 +16,10 @@
The following is an example of a string that could be used to cause remote code execution when interpreted as a template:
-
+
In the following case, user input is not used to construct the template; rather is only used for as the parameters to render the template, which is safe.
-
+
In the following case, a SandboxedEnvironment is used, preventing remote code execution.
diff --git a/python/ql/src/experimental/Security/CWE-074/JinjaBad.py b/python/ql/src/experimental/Security/CWE-074/JinjaBad.py
deleted file mode 100644
index aaac3ec819eb..000000000000
--- a/python/ql/src/experimental/Security/CWE-074/JinjaBad.py
+++ /dev/null
@@ -1,19 +0,0 @@
-from django.urls import path
-from django.http import HttpResponse
-from jinja2 import Template as Jinja2_Template
-from jinja2 import Environment, DictLoader, escape
-
-
-def a(request):
- # Load the template
- template = request.GET['template']
- t = Jinja2_Template(template)
- name = request.GET['name']
- # Render the template with the context data
- html = t.render(name=escape(name))
- return HttpResponse(html)
-
-
-urlpatterns = [
- path('a', a),
-]
diff --git a/python/ql/src/experimental/Security/CWE-074/JinjaGood.py b/python/ql/src/experimental/Security/CWE-074/JinjaGood.py
deleted file mode 100644
index a1b605618501..000000000000
--- a/python/ql/src/experimental/Security/CWE-074/JinjaGood.py
+++ /dev/null
@@ -1,20 +0,0 @@
-from django.urls import path
-from django.http import HttpResponse
-from jinja2 import Template as Jinja2_Template
-from jinja2 import Environment, DictLoader, escape
-
-
-def a(request):
- # Load the template
- template = request.GET['template']
- env = SandboxedEnvironment(undefined=StrictUndefined)
- t = env.from_string(template)
- name = request.GET['name']
- # Render the template with the context data
- html = t.render(name=escape(name))
- return HttpResponse(html)
-
-
-urlpatterns = [
- path('a', a),
-]
diff --git a/python/ql/src/experimental/Security/CWE-074/TemplateConstructionConcept.qll b/python/ql/src/experimental/Security/CWE-074/TemplateConstructionConcept.qll
deleted file mode 100644
index 5144e2ff97b1..000000000000
--- a/python/ql/src/experimental/Security/CWE-074/TemplateConstructionConcept.qll
+++ /dev/null
@@ -1,165 +0,0 @@
-private import python
-private import semmle.python.dataflow.new.DataFlow
-private import semmle.python.ApiGraphs
-
-/**
- * A data-flow node that constructs a template.
- *
- * Extend this class to refine existing API models. If you want to model new APIs,
- * extend `TemplateConstruction::Range` instead.
- */
-class TemplateConstruction extends DataFlow::Node instanceof TemplateConstruction::Range {
- /** Gets the argument that specifies the template source. */
- DataFlow::Node getSourceArg() { result = super.getSourceArg() }
-}
-
-/** Provides a class for modeling new system-command execution APIs. */
-module TemplateConstruction {
- /**
- * A data-flow node that constructs a template.
- *
- * Extend this class to model new APIs. If you want to refine existing API models,
- * extend `TemplateConstruction` instead.
- */
- abstract class Range extends DataFlow::Node {
- /** Gets the argument that specifies the template source. */
- abstract DataFlow::Node getSourceArg();
- }
-}
-
-// -----------------------------------------------------------------------------
-/** A call to `airspeed.Template`. */
-class AirspeedTemplateConstruction extends TemplateConstruction::Range, API::CallNode {
- AirspeedTemplateConstruction() {
- this = API::moduleImport("airspeed").getMember("Template").getACall()
- }
-
- override DataFlow::Node getSourceArg() { result = this.getArg(0) }
-}
-
-/** A call to `bottle.SimpleTemplate`. */
-class BottleSimpleTemplateConstruction extends TemplateConstruction::Range, API::CallNode {
- BottleSimpleTemplateConstruction() {
- this = API::moduleImport("bottle").getMember("SimpleTemplate").getACall()
- }
-
- override DataFlow::Node getSourceArg() { result = this.getArg(0) }
-}
-
-/** A call to `bottle.template`. */
-class BottleTemplateConstruction extends TemplateConstruction::Range, API::CallNode {
- BottleTemplateConstruction() {
- this = API::moduleImport("bottle").getMember("template").getACall()
- }
-
- override DataFlow::Node getSourceArg() { result = this.getArg(0) }
-}
-
-/** A call to `chameleon.PageTemplate`. */
-class ChameleonTemplateConstruction extends TemplateConstruction::Range, API::CallNode {
- ChameleonTemplateConstruction() {
- this = API::moduleImport("chameleon").getMember("PageTemplate").getACall()
- }
-
- override DataFlow::Node getSourceArg() { result = this.getArg(0) }
-}
-
-/** A call to `Cheetah.Template.Template`. */
-class CheetahTemplateConstruction extends TemplateConstruction::Range, API::CallNode {
- CheetahTemplateConstruction() {
- this =
- API::moduleImport("Cheetah")
- .getMember("Template")
- .getMember("Template")
- .getASubclass*()
- .getACall()
- }
-
- override DataFlow::Node getSourceArg() { result = this.getArg(0) }
-}
-
-/** A call to `chevron.render`. */
-class ChevronRenderConstruction extends TemplateConstruction::Range, API::CallNode {
- ChevronRenderConstruction() { this = API::moduleImport("chevron").getMember("render").getACall() }
-
- override DataFlow::Node getSourceArg() { result = this.getArg(0) }
-}
-
-/** A call to `django.template.Template` */
-class DjangoTemplateConstruction extends TemplateConstruction::Range, API::CallNode {
- DjangoTemplateConstruction() {
- this = API::moduleImport("django").getMember("template").getMember("Template").getACall()
- }
-
- override DataFlow::Node getSourceArg() { result = this.getArg(0) }
-}
-
-// TODO: support django.template.engines["django"]].from_string
-/** A call to `flask.render_template_string`. */
-class FlaskTemplateConstruction extends TemplateConstruction::Range, API::CallNode {
- FlaskTemplateConstruction() {
- this = API::moduleImport("flask").getMember("render_template_string").getACall()
- }
-
- override DataFlow::Node getSourceArg() { result = this.getArg(0) }
-}
-
-/** A call to `genshi.template.TextTemplate`. */
-class GenshiTextTemplateConstruction extends TemplateConstruction::Range, API::CallNode {
- GenshiTextTemplateConstruction() {
- this = API::moduleImport("genshi").getMember("template").getMember("TextTemplate").getACall()
- }
-
- override DataFlow::Node getSourceArg() { result = this.getArg(0) }
-}
-
-/** A call to `genshi.template.MarkupTemplate` */
-class GenshiMarkupTemplateConstruction extends TemplateConstruction::Range, API::CallNode {
- GenshiMarkupTemplateConstruction() {
- this = API::moduleImport("genshi").getMember("template").getMember("MarkupTemplate").getACall()
- }
-
- override DataFlow::Node getSourceArg() { result = this.getArg(0) }
-}
-
-//
-/** A call to `jinja2.Template`. */
-class Jinja2TemplateConstruction extends TemplateConstruction::Range, API::CallNode {
- Jinja2TemplateConstruction() {
- this = API::moduleImport("jinja2").getMember("Template").getACall()
- }
-
- override DataFlow::Node getSourceArg() { result = this.getArg(0) }
-}
-
-/** A call to `jinja2.from_string`. */
-class Jinja2FromStringConstruction extends TemplateConstruction::Range, API::CallNode {
- Jinja2FromStringConstruction() {
- this =
- API::moduleImport("jinja2")
- .getMember("Environment")
- .getReturn()
- .getMember("from_string")
- .getACall()
- }
-
- override DataFlow::Node getSourceArg() { result = this.getArg(0) }
-}
-
-/** A call to `mako.template.Template`. */
-class MakoTemplateConstruction extends TemplateConstruction::Range, API::CallNode {
- MakoTemplateConstruction() {
- this = API::moduleImport("mako").getMember("template").getMember("Template").getACall()
- }
-
- override DataFlow::Node getSourceArg() { result = this.getArg(0) }
-}
-
-/** A call to `trender.TRender`. */
-class TRenderTemplateConstruction extends TemplateConstruction::Range, API::CallNode {
- TRenderTemplateConstruction() {
- this = API::moduleImport("trender").getMember("TRender").getACall()
- }
-
- override DataFlow::Node getSourceArg() { result = this.getArg(0) }
-}
diff --git a/python/ql/src/experimental/Security/CWE-074/TemplateInjection.qhelp b/python/ql/src/experimental/Security/CWE-074/TemplateInjection.qhelp
deleted file mode 100644
index b044243fc8e1..000000000000
--- a/python/ql/src/experimental/Security/CWE-074/TemplateInjection.qhelp
+++ /dev/null
@@ -1,24 +0,0 @@
-
-
-
-
- Template Injection occurs when user input is embedded in a template in an unsafe manner.
- When an attacker is able to use native template syntax to inject a malicious payload into a template, which is then executed server-side is results in Server Side Template Injection.
-
-
-
-
- To fix this, ensure that an untrusted value is not used as a template. If the application requirements do not alow this, use a sandboxed environment where access to unsafe attributes and methods is prohibited.
-
-
-
- Consider the example given below, an untrusted HTTP parameter `template` is used to generate a Jinja2 template string. This can lead to remote code execution.
-
-
- Here we have fixed the problem by using the Jinja sandbox environment for evaluating untrusted code.
-
-
-
- Portswigger : [Server Side Template Injection](https://portswigger.net/web-security/server-side-template-injection)
-
-
diff --git a/python/ql/src/experimental/Security/CWE-074/TemplateInjection.ql b/python/ql/src/experimental/Security/CWE-074/TemplateInjection.ql
deleted file mode 100644
index a10ad09a6ac9..000000000000
--- a/python/ql/src/experimental/Security/CWE-074/TemplateInjection.ql
+++ /dev/null
@@ -1,20 +0,0 @@
-/**
- * @name Server Side Template Injection
- * @description Using user-controlled data to create a template can cause security issues.
- * @kind path-problem
- * @problem.severity error
- * @precision high
- * @id py/template-injection
- * @tags security
- * experimental
- * external/cwe/cwe-074
- */
-
-import python
-import TemplateInjectionQuery
-import TemplateInjectionFlow::PathGraph
-
-from TemplateInjectionFlow::PathNode source, TemplateInjectionFlow::PathNode sink
-where TemplateInjectionFlow::flowPath(source, sink)
-select sink.getNode(), source, sink, "This Template depends on $@.", source.getNode(),
- "user-provided value"
diff --git a/python/ql/src/experimental/Security/CWE-074/TemplateInjectionCustomizations.qll b/python/ql/src/experimental/Security/CWE-074/TemplateInjectionCustomizations.qll
deleted file mode 100644
index 13c70fc7d04d..000000000000
--- a/python/ql/src/experimental/Security/CWE-074/TemplateInjectionCustomizations.qll
+++ /dev/null
@@ -1,59 +0,0 @@
-/**
- * Provides default sources, sinks and sanitizers for detecting
- * "template injection"
- * vulnerabilities, as well as extension points for adding your own.
- */
-
-private import python
-private import semmle.python.dataflow.new.DataFlow
-private import semmle.python.Concepts as C
-private import semmle.python.dataflow.new.RemoteFlowSources
-private import semmle.python.dataflow.new.BarrierGuards
-private import TemplateConstructionConcept
-
-/**
- * Provides default sources, sinks and sanitizers for detecting
- * "template injection"
- * vulnerabilities, as well as extension points for adding your own.
- */
-module TemplateInjection {
- /**
- * A data flow source for "template injection" vulnerabilities.
- */
- abstract class Source extends DataFlow::Node { }
-
- /**
- * A data flow sink for "template injection" vulnerabilities.
- */
- abstract class Sink extends DataFlow::Node { }
-
- /**
- * A sanitizer for "template injection" vulnerabilities.
- */
- abstract class Sanitizer extends DataFlow::Node { }
-
- /**
- * DEPRECATED: Use `ActiveThreatModelSource` from Concepts instead!
- */
- deprecated class RemoteFlowSourceAsSource = ActiveThreatModelSourceAsSource;
-
- /**
- * An active threat-model source, considered as a flow source.
- */
- private class ActiveThreatModelSourceAsSource extends Source, C::ActiveThreatModelSource { }
-
- /**
- * A SQL statement of a SQL construction, considered as a flow sink.
- */
- class TemplateConstructionAsSink extends Sink {
- TemplateConstructionAsSink() { this = any(TemplateConstruction c).getSourceArg() }
- }
-
- /**
- * A comparison with a constant, considered as a sanitizer-guard.
- */
- class ConstCompareAsSanitizerGuard extends Sanitizer, ConstCompareBarrier { }
-
- /** DEPRECATED: Use ConstCompareAsSanitizerGuard instead. */
- deprecated class StringConstCompareAsSanitizerGuard = ConstCompareAsSanitizerGuard;
-}
diff --git a/python/ql/src/experimental/Security/CWE-074/TemplateInjectionQuery.qll b/python/ql/src/experimental/Security/CWE-074/TemplateInjectionQuery.qll
deleted file mode 100644
index 111485e2602d..000000000000
--- a/python/ql/src/experimental/Security/CWE-074/TemplateInjectionQuery.qll
+++ /dev/null
@@ -1,18 +0,0 @@
-/**
- * Provides a taint-tracking configuration for detecting "template injection" vulnerabilities.
- */
-
-private import python
-import semmle.python.dataflow.new.DataFlow
-import semmle.python.dataflow.new.TaintTracking
-import TemplateInjectionCustomizations::TemplateInjection
-
-module TemplateInjectionConfig implements DataFlow::ConfigSig {
- predicate isSource(DataFlow::Node node) { node instanceof Source }
-
- predicate isSink(DataFlow::Node node) { node instanceof Sink }
-
- predicate isBarrierIn(DataFlow::Node node) { node instanceof Sanitizer }
-}
-
-module TemplateInjectionFlow = TaintTracking::Global;
diff --git a/python/ql/test/experimental/query-tests/Security/CWE-074-TemplateInjection/AirspeedSsti.py b/python/ql/test/experimental/query-tests/Security/CWE-074-TemplateInjection/AirspeedSsti.py
deleted file mode 100644
index 8938d8602f8d..000000000000
--- a/python/ql/test/experimental/query-tests/Security/CWE-074-TemplateInjection/AirspeedSsti.py
+++ /dev/null
@@ -1,11 +0,0 @@
-import airspeed
-from flask import Flask, request
-
-
-app = Flask(__name__)
-
-
-@route('/other')
-def a():
- template = request.args.get('template')
- return airspeed.Template(template)
diff --git a/python/ql/test/experimental/query-tests/Security/CWE-074-TemplateInjection/BottleSsti.py b/python/ql/test/experimental/query-tests/Security/CWE-074-TemplateInjection/BottleSsti.py
deleted file mode 100644
index b5f8a5feeffa..000000000000
--- a/python/ql/test/experimental/query-tests/Security/CWE-074-TemplateInjection/BottleSsti.py
+++ /dev/null
@@ -1,20 +0,0 @@
-from bottle import Bottle, route, request, redirect, response, SimpleTemplate
-from bottle import template as temp
-
-
-app = Bottle()
-
-
-@route('/other')
-def a():
- template = request.query.template
- tpl = SimpleTemplate(template)
- tpl.render(name='World')
- return tmp
-
-
-@route('/other2')
-def b():
- template = request.query.template
- temp(template, name='World')
- return tmp
diff --git a/python/ql/test/experimental/query-tests/Security/CWE-074-TemplateInjection/Chameleon.py b/python/ql/test/experimental/query-tests/Security/CWE-074-TemplateInjection/Chameleon.py
deleted file mode 100644
index f58a641a9be3..000000000000
--- a/python/ql/test/experimental/query-tests/Security/CWE-074-TemplateInjection/Chameleon.py
+++ /dev/null
@@ -1,10 +0,0 @@
-from chameleon import PageTemplate
-from django.urls import path
-from django.http import HttpResponse
-
-
-def chameleon(request):
- template = request.GET['template']
- tmpl = PageTemplate(template)
- return HttpResponse(tmpl)
-
diff --git a/python/ql/test/experimental/query-tests/Security/CWE-074-TemplateInjection/CheetahSinks.py b/python/ql/test/experimental/query-tests/Security/CWE-074-TemplateInjection/CheetahSinks.py
deleted file mode 100644
index 7f9fed4decf5..000000000000
--- a/python/ql/test/experimental/query-tests/Security/CWE-074-TemplateInjection/CheetahSinks.py
+++ /dev/null
@@ -1,22 +0,0 @@
-from flask import Flask, request
-from Cheetah.Template import Template
-
-
-app = Flask(__name__)
-
-
-@app.route('/other')
-def a():
- template = request.args.get('template')
- return Template(template)
-
-
-class Template3(Template):
- title = 'Hello World Example!'
- contents = 'Hello World!'
-
-
-@app.route('/other2')
-def b():
- template = request.args.get('template')
- t3 = Template3(template)
diff --git a/python/ql/test/experimental/query-tests/Security/CWE-074-TemplateInjection/ChevronSsti.py b/python/ql/test/experimental/query-tests/Security/CWE-074-TemplateInjection/ChevronSsti.py
deleted file mode 100644
index f3b0e57fc8f7..000000000000
--- a/python/ql/test/experimental/query-tests/Security/CWE-074-TemplateInjection/ChevronSsti.py
+++ /dev/null
@@ -1,24 +0,0 @@
-from flask import Flask, request
-import chevron
-
-
-app = Flask(__name__)
-
-
-@app.route('/other')
-def a():
- template = request.args.get('template')
- return chevron.render(template, {"key": "value"})
-
-
-@app.route('/other2')
-def b():
- template = request.args.get('template')
- args = {
- 'template': template,
-
- 'data': {
- 'key': 'value'
- }
- }
- return chevron.render(**args)
diff --git a/python/ql/test/experimental/query-tests/Security/CWE-074-TemplateInjection/DjangoTemplates.py b/python/ql/test/experimental/query-tests/Security/CWE-074-TemplateInjection/DjangoTemplates.py
deleted file mode 100644
index 26f48fd92780..000000000000
--- a/python/ql/test/experimental/query-tests/Security/CWE-074-TemplateInjection/DjangoTemplates.py
+++ /dev/null
@@ -1,41 +0,0 @@
-from django.urls import path
-from django.http import HttpResponse
-from django.template import Template, Context, Engine, engines
-
-
-def dj(request):
- # Load the template
- template = request.GET['template']
- t = Template(template)
- ctx = Context(locals())
- html = t.render(ctx)
- return HttpResponse(html)
-
-
-def djEngine(request):
- # Load the template
- template = request.GET['template']
-
- django_engine = engines['django']
- t = django_engine.from_string(template)
- ctx = Context(locals())
- html = t.render(ctx)
- return HttpResponse(html)
-
-
-def djEngineJinja(request):
- # Load the template
- template = request.GET['template']
-
- django_engine = engines['jinja']
- t = django_engine.from_string(template)
- ctx = Context(locals())
- html = t.render(ctx)
- return HttpResponse(html)
-
-
-urlpatterns = [
- path('', dj),
- path('', djEngine),
- path('', djEngineJinja),
-]
diff --git a/python/ql/test/experimental/query-tests/Security/CWE-074-TemplateInjection/FlaskTemplate.py b/python/ql/test/experimental/query-tests/Security/CWE-074-TemplateInjection/FlaskTemplate.py
deleted file mode 100644
index b74e3cce715d..000000000000
--- a/python/ql/test/experimental/query-tests/Security/CWE-074-TemplateInjection/FlaskTemplate.py
+++ /dev/null
@@ -1,22 +0,0 @@
-from flask import Flask, request
-
-
-app = Flask(__name__)
-
-
-@app.route("/")
-def home():
- from flask import render_template_string
- if request.args.get('template'):
- return render_template_string(request.args.get('template'))
-
-
-@app.route("/a")
-def a():
- import flask
- return flask.render_template_string(request.args.get('template'))
-
-
-
-if __name__ == "__main__":
- app.run(debug=True)
diff --git a/python/ql/test/experimental/query-tests/Security/CWE-074-TemplateInjection/Genshi.py b/python/ql/test/experimental/query-tests/Security/CWE-074-TemplateInjection/Genshi.py
deleted file mode 100644
index 7800c50da968..000000000000
--- a/python/ql/test/experimental/query-tests/Security/CWE-074-TemplateInjection/Genshi.py
+++ /dev/null
@@ -1,18 +0,0 @@
-from django.urls import path
-from django.http import HttpResponse
-from genshi.template import TextTemplate,MarkupTemplate
-
-def genshi1():
- template = request.GET['template']
- tmpl = MarkupTemplate(template)
- return HttpResponse(tmpl)
-
-def genshi2():
- template = request.GET['template']
- tmpl = TextTemplate(template)
- return HttpResponse(tmpl)
-
-urlpatterns = [
- path('', genshi1),
- path('', genshi2)
-]
diff --git a/python/ql/test/experimental/query-tests/Security/CWE-074-TemplateInjection/JinjaSsti.py b/python/ql/test/experimental/query-tests/Security/CWE-074-TemplateInjection/JinjaSsti.py
deleted file mode 100644
index 28225c81cbaa..000000000000
--- a/python/ql/test/experimental/query-tests/Security/CWE-074-TemplateInjection/JinjaSsti.py
+++ /dev/null
@@ -1,30 +0,0 @@
-from django.urls import path
-from django.http import HttpResponse
-from jinja2 import Template as Jinja2_Template
-from jinja2 import Environment, DictLoader, escape
-
-
-def a(request):
- # Load the template
- template = request.GET['template']
- t = Jinja2_Template(template)
- name = request.GET['name']
- # Render the template with the context data
- html = t.render(name=escape(name))
- return HttpResponse(html)
-
-def b(request):
- import jinja2
- # Load the template
- template = request.GET['template']
- t = jinja2.from_string(template)
- name = request.GET['name']
- # Render the template with the context data
- html = t.render(name=escape(name))
- return HttpResponse(html)
-
-
-urlpatterns = [
- path('a', a),
- path('b', b)
-]
diff --git a/python/ql/test/experimental/query-tests/Security/CWE-074-TemplateInjection/MakoSsti.py b/python/ql/test/experimental/query-tests/Security/CWE-074-TemplateInjection/MakoSsti.py
deleted file mode 100644
index 7f6b25cb26cb..000000000000
--- a/python/ql/test/experimental/query-tests/Security/CWE-074-TemplateInjection/MakoSsti.py
+++ /dev/null
@@ -1,15 +0,0 @@
-from django.urls import path
-from django.http import HttpResponse
-from mako.template import Template
-
-
-def mako(request):
- # Load the template
- template = request.GET['template']
- mytemplate = Template(template)
- return HttpResponse(mytemplate)
-
-
-urlpatterns = [
- path('', mako)
-]
diff --git a/python/ql/test/experimental/query-tests/Security/CWE-074-TemplateInjection/TRender.py b/python/ql/test/experimental/query-tests/Security/CWE-074-TemplateInjection/TRender.py
deleted file mode 100644
index 2514f22b8059..000000000000
--- a/python/ql/test/experimental/query-tests/Security/CWE-074-TemplateInjection/TRender.py
+++ /dev/null
@@ -1,12 +0,0 @@
-from django.urls import path
-from django.http import HttpResponse
-from trender import TRender
-
-def trender(request):
- template = request.GET['template']
- compiled = TRender(template)
- return HttpResponse(compiled)
-
-urlpatterns = [
- path('', trender)
-]
diff --git a/python/ql/test/experimental/query-tests/Security/CWE-074-TemplateInjection/TemplateInjection.expected b/python/ql/test/experimental/query-tests/Security/CWE-074-TemplateInjection/TemplateInjection.expected
deleted file mode 100644
index 06cf81cc6aaf..000000000000
--- a/python/ql/test/experimental/query-tests/Security/CWE-074-TemplateInjection/TemplateInjection.expected
+++ /dev/null
@@ -1,107 +0,0 @@
-edges
-| AirspeedSsti.py:2:26:2:32 | ControlFlowNode for ImportMember | AirspeedSsti.py:2:26:2:32 | ControlFlowNode for request | provenance | |
-| AirspeedSsti.py:2:26:2:32 | ControlFlowNode for request | AirspeedSsti.py:10:16:10:22 | ControlFlowNode for request | provenance | |
-| AirspeedSsti.py:10:5:10:12 | ControlFlowNode for template | AirspeedSsti.py:11:30:11:37 | ControlFlowNode for template | provenance | |
-| AirspeedSsti.py:10:16:10:22 | ControlFlowNode for request | AirspeedSsti.py:10:16:10:27 | ControlFlowNode for Attribute | provenance | AdditionalTaintStep |
-| AirspeedSsti.py:10:16:10:27 | ControlFlowNode for Attribute | AirspeedSsti.py:10:16:10:43 | ControlFlowNode for Attribute() | provenance | dict.get |
-| AirspeedSsti.py:10:16:10:43 | ControlFlowNode for Attribute() | AirspeedSsti.py:10:5:10:12 | ControlFlowNode for template | provenance | |
-| CheetahSinks.py:1:26:1:32 | ControlFlowNode for ImportMember | CheetahSinks.py:1:26:1:32 | ControlFlowNode for request | provenance | |
-| CheetahSinks.py:1:26:1:32 | ControlFlowNode for request | CheetahSinks.py:10:16:10:22 | ControlFlowNode for request | provenance | |
-| CheetahSinks.py:1:26:1:32 | ControlFlowNode for request | CheetahSinks.py:21:16:21:22 | ControlFlowNode for request | provenance | |
-| CheetahSinks.py:10:5:10:12 | ControlFlowNode for template | CheetahSinks.py:11:21:11:28 | ControlFlowNode for template | provenance | |
-| CheetahSinks.py:10:16:10:22 | ControlFlowNode for request | CheetahSinks.py:10:16:10:27 | ControlFlowNode for Attribute | provenance | AdditionalTaintStep |
-| CheetahSinks.py:10:16:10:27 | ControlFlowNode for Attribute | CheetahSinks.py:10:16:10:43 | ControlFlowNode for Attribute() | provenance | dict.get |
-| CheetahSinks.py:10:16:10:43 | ControlFlowNode for Attribute() | CheetahSinks.py:10:5:10:12 | ControlFlowNode for template | provenance | |
-| CheetahSinks.py:21:5:21:12 | ControlFlowNode for template | CheetahSinks.py:22:20:22:27 | ControlFlowNode for template | provenance | |
-| CheetahSinks.py:21:16:21:22 | ControlFlowNode for request | CheetahSinks.py:21:16:21:27 | ControlFlowNode for Attribute | provenance | AdditionalTaintStep |
-| CheetahSinks.py:21:16:21:27 | ControlFlowNode for Attribute | CheetahSinks.py:21:16:21:43 | ControlFlowNode for Attribute() | provenance | dict.get |
-| CheetahSinks.py:21:16:21:43 | ControlFlowNode for Attribute() | CheetahSinks.py:21:5:21:12 | ControlFlowNode for template | provenance | |
-| ChevronSsti.py:1:26:1:32 | ControlFlowNode for ImportMember | ChevronSsti.py:1:26:1:32 | ControlFlowNode for request | provenance | |
-| ChevronSsti.py:1:26:1:32 | ControlFlowNode for request | ChevronSsti.py:10:16:10:22 | ControlFlowNode for request | provenance | |
-| ChevronSsti.py:10:5:10:12 | ControlFlowNode for template | ChevronSsti.py:11:27:11:34 | ControlFlowNode for template | provenance | |
-| ChevronSsti.py:10:16:10:22 | ControlFlowNode for request | ChevronSsti.py:10:16:10:27 | ControlFlowNode for Attribute | provenance | AdditionalTaintStep |
-| ChevronSsti.py:10:16:10:27 | ControlFlowNode for Attribute | ChevronSsti.py:10:16:10:43 | ControlFlowNode for Attribute() | provenance | dict.get |
-| ChevronSsti.py:10:16:10:43 | ControlFlowNode for Attribute() | ChevronSsti.py:10:5:10:12 | ControlFlowNode for template | provenance | |
-| DjangoTemplates.py:6:8:6:14 | ControlFlowNode for request | DjangoTemplates.py:8:5:8:12 | ControlFlowNode for template | provenance | AdditionalTaintStep |
-| DjangoTemplates.py:8:5:8:12 | ControlFlowNode for template | DjangoTemplates.py:9:18:9:25 | ControlFlowNode for template | provenance | |
-| FlaskTemplate.py:1:26:1:32 | ControlFlowNode for ImportMember | FlaskTemplate.py:1:26:1:32 | ControlFlowNode for request | provenance | |
-| FlaskTemplate.py:1:26:1:32 | ControlFlowNode for request | FlaskTemplate.py:10:8:10:14 | ControlFlowNode for request | provenance | |
-| FlaskTemplate.py:1:26:1:32 | ControlFlowNode for request | FlaskTemplate.py:11:39:11:45 | ControlFlowNode for request | provenance | |
-| FlaskTemplate.py:1:26:1:32 | ControlFlowNode for request | FlaskTemplate.py:17:41:17:47 | ControlFlowNode for request | provenance | |
-| FlaskTemplate.py:10:8:10:14 | ControlFlowNode for request | FlaskTemplate.py:11:39:11:50 | ControlFlowNode for Attribute | provenance | AdditionalTaintStep |
-| FlaskTemplate.py:11:39:11:45 | ControlFlowNode for request | FlaskTemplate.py:11:39:11:50 | ControlFlowNode for Attribute | provenance | AdditionalTaintStep |
-| FlaskTemplate.py:11:39:11:50 | ControlFlowNode for Attribute | FlaskTemplate.py:11:39:11:66 | ControlFlowNode for Attribute() | provenance | dict.get |
-| FlaskTemplate.py:17:41:17:47 | ControlFlowNode for request | FlaskTemplate.py:17:41:17:52 | ControlFlowNode for Attribute | provenance | AdditionalTaintStep |
-| FlaskTemplate.py:17:41:17:52 | ControlFlowNode for Attribute | FlaskTemplate.py:17:41:17:68 | ControlFlowNode for Attribute() | provenance | dict.get |
-| JinjaSsti.py:7:7:7:13 | ControlFlowNode for request | JinjaSsti.py:9:5:9:12 | ControlFlowNode for template | provenance | AdditionalTaintStep |
-| JinjaSsti.py:9:5:9:12 | ControlFlowNode for template | JinjaSsti.py:10:25:10:32 | ControlFlowNode for template | provenance | |
-| JinjaSsti.py:16:7:16:13 | ControlFlowNode for request | JinjaSsti.py:19:5:19:12 | ControlFlowNode for template | provenance | AdditionalTaintStep |
-| JinjaSsti.py:19:5:19:12 | ControlFlowNode for template | JinjaSsti.py:20:28:20:35 | ControlFlowNode for template | provenance | |
-| MakoSsti.py:6:10:6:16 | ControlFlowNode for request | MakoSsti.py:8:5:8:12 | ControlFlowNode for template | provenance | AdditionalTaintStep |
-| MakoSsti.py:8:5:8:12 | ControlFlowNode for template | MakoSsti.py:9:27:9:34 | ControlFlowNode for template | provenance | |
-| TRender.py:5:13:5:19 | ControlFlowNode for request | TRender.py:6:5:6:12 | ControlFlowNode for template | provenance | AdditionalTaintStep |
-| TRender.py:6:5:6:12 | ControlFlowNode for template | TRender.py:7:24:7:31 | ControlFlowNode for template | provenance | |
-nodes
-| AirspeedSsti.py:2:26:2:32 | ControlFlowNode for ImportMember | semmle.label | ControlFlowNode for ImportMember |
-| AirspeedSsti.py:2:26:2:32 | ControlFlowNode for request | semmle.label | ControlFlowNode for request |
-| AirspeedSsti.py:10:5:10:12 | ControlFlowNode for template | semmle.label | ControlFlowNode for template |
-| AirspeedSsti.py:10:16:10:22 | ControlFlowNode for request | semmle.label | ControlFlowNode for request |
-| AirspeedSsti.py:10:16:10:27 | ControlFlowNode for Attribute | semmle.label | ControlFlowNode for Attribute |
-| AirspeedSsti.py:10:16:10:43 | ControlFlowNode for Attribute() | semmle.label | ControlFlowNode for Attribute() |
-| AirspeedSsti.py:11:30:11:37 | ControlFlowNode for template | semmle.label | ControlFlowNode for template |
-| CheetahSinks.py:1:26:1:32 | ControlFlowNode for ImportMember | semmle.label | ControlFlowNode for ImportMember |
-| CheetahSinks.py:1:26:1:32 | ControlFlowNode for request | semmle.label | ControlFlowNode for request |
-| CheetahSinks.py:10:5:10:12 | ControlFlowNode for template | semmle.label | ControlFlowNode for template |
-| CheetahSinks.py:10:16:10:22 | ControlFlowNode for request | semmle.label | ControlFlowNode for request |
-| CheetahSinks.py:10:16:10:27 | ControlFlowNode for Attribute | semmle.label | ControlFlowNode for Attribute |
-| CheetahSinks.py:10:16:10:43 | ControlFlowNode for Attribute() | semmle.label | ControlFlowNode for Attribute() |
-| CheetahSinks.py:11:21:11:28 | ControlFlowNode for template | semmle.label | ControlFlowNode for template |
-| CheetahSinks.py:21:5:21:12 | ControlFlowNode for template | semmle.label | ControlFlowNode for template |
-| CheetahSinks.py:21:16:21:22 | ControlFlowNode for request | semmle.label | ControlFlowNode for request |
-| CheetahSinks.py:21:16:21:27 | ControlFlowNode for Attribute | semmle.label | ControlFlowNode for Attribute |
-| CheetahSinks.py:21:16:21:43 | ControlFlowNode for Attribute() | semmle.label | ControlFlowNode for Attribute() |
-| CheetahSinks.py:22:20:22:27 | ControlFlowNode for template | semmle.label | ControlFlowNode for template |
-| ChevronSsti.py:1:26:1:32 | ControlFlowNode for ImportMember | semmle.label | ControlFlowNode for ImportMember |
-| ChevronSsti.py:1:26:1:32 | ControlFlowNode for request | semmle.label | ControlFlowNode for request |
-| ChevronSsti.py:10:5:10:12 | ControlFlowNode for template | semmle.label | ControlFlowNode for template |
-| ChevronSsti.py:10:16:10:22 | ControlFlowNode for request | semmle.label | ControlFlowNode for request |
-| ChevronSsti.py:10:16:10:27 | ControlFlowNode for Attribute | semmle.label | ControlFlowNode for Attribute |
-| ChevronSsti.py:10:16:10:43 | ControlFlowNode for Attribute() | semmle.label | ControlFlowNode for Attribute() |
-| ChevronSsti.py:11:27:11:34 | ControlFlowNode for template | semmle.label | ControlFlowNode for template |
-| DjangoTemplates.py:6:8:6:14 | ControlFlowNode for request | semmle.label | ControlFlowNode for request |
-| DjangoTemplates.py:8:5:8:12 | ControlFlowNode for template | semmle.label | ControlFlowNode for template |
-| DjangoTemplates.py:9:18:9:25 | ControlFlowNode for template | semmle.label | ControlFlowNode for template |
-| FlaskTemplate.py:1:26:1:32 | ControlFlowNode for ImportMember | semmle.label | ControlFlowNode for ImportMember |
-| FlaskTemplate.py:1:26:1:32 | ControlFlowNode for request | semmle.label | ControlFlowNode for request |
-| FlaskTemplate.py:10:8:10:14 | ControlFlowNode for request | semmle.label | ControlFlowNode for request |
-| FlaskTemplate.py:11:39:11:45 | ControlFlowNode for request | semmle.label | ControlFlowNode for request |
-| FlaskTemplate.py:11:39:11:50 | ControlFlowNode for Attribute | semmle.label | ControlFlowNode for Attribute |
-| FlaskTemplate.py:11:39:11:66 | ControlFlowNode for Attribute() | semmle.label | ControlFlowNode for Attribute() |
-| FlaskTemplate.py:17:41:17:47 | ControlFlowNode for request | semmle.label | ControlFlowNode for request |
-| FlaskTemplate.py:17:41:17:52 | ControlFlowNode for Attribute | semmle.label | ControlFlowNode for Attribute |
-| FlaskTemplate.py:17:41:17:68 | ControlFlowNode for Attribute() | semmle.label | ControlFlowNode for Attribute() |
-| JinjaSsti.py:7:7:7:13 | ControlFlowNode for request | semmle.label | ControlFlowNode for request |
-| JinjaSsti.py:9:5:9:12 | ControlFlowNode for template | semmle.label | ControlFlowNode for template |
-| JinjaSsti.py:10:25:10:32 | ControlFlowNode for template | semmle.label | ControlFlowNode for template |
-| JinjaSsti.py:16:7:16:13 | ControlFlowNode for request | semmle.label | ControlFlowNode for request |
-| JinjaSsti.py:19:5:19:12 | ControlFlowNode for template | semmle.label | ControlFlowNode for template |
-| JinjaSsti.py:20:28:20:35 | ControlFlowNode for template | semmle.label | ControlFlowNode for template |
-| MakoSsti.py:6:10:6:16 | ControlFlowNode for request | semmle.label | ControlFlowNode for request |
-| MakoSsti.py:8:5:8:12 | ControlFlowNode for template | semmle.label | ControlFlowNode for template |
-| MakoSsti.py:9:27:9:34 | ControlFlowNode for template | semmle.label | ControlFlowNode for template |
-| TRender.py:5:13:5:19 | ControlFlowNode for request | semmle.label | ControlFlowNode for request |
-| TRender.py:6:5:6:12 | ControlFlowNode for template | semmle.label | ControlFlowNode for template |
-| TRender.py:7:24:7:31 | ControlFlowNode for template | semmle.label | ControlFlowNode for template |
-subpaths
-#select
-| AirspeedSsti.py:11:30:11:37 | ControlFlowNode for template | AirspeedSsti.py:2:26:2:32 | ControlFlowNode for ImportMember | AirspeedSsti.py:11:30:11:37 | ControlFlowNode for template | This Template depends on $@. | AirspeedSsti.py:2:26:2:32 | ControlFlowNode for ImportMember | user-provided value |
-| CheetahSinks.py:11:21:11:28 | ControlFlowNode for template | CheetahSinks.py:1:26:1:32 | ControlFlowNode for ImportMember | CheetahSinks.py:11:21:11:28 | ControlFlowNode for template | This Template depends on $@. | CheetahSinks.py:1:26:1:32 | ControlFlowNode for ImportMember | user-provided value |
-| CheetahSinks.py:22:20:22:27 | ControlFlowNode for template | CheetahSinks.py:1:26:1:32 | ControlFlowNode for ImportMember | CheetahSinks.py:22:20:22:27 | ControlFlowNode for template | This Template depends on $@. | CheetahSinks.py:1:26:1:32 | ControlFlowNode for ImportMember | user-provided value |
-| ChevronSsti.py:11:27:11:34 | ControlFlowNode for template | ChevronSsti.py:1:26:1:32 | ControlFlowNode for ImportMember | ChevronSsti.py:11:27:11:34 | ControlFlowNode for template | This Template depends on $@. | ChevronSsti.py:1:26:1:32 | ControlFlowNode for ImportMember | user-provided value |
-| DjangoTemplates.py:9:18:9:25 | ControlFlowNode for template | DjangoTemplates.py:6:8:6:14 | ControlFlowNode for request | DjangoTemplates.py:9:18:9:25 | ControlFlowNode for template | This Template depends on $@. | DjangoTemplates.py:6:8:6:14 | ControlFlowNode for request | user-provided value |
-| FlaskTemplate.py:11:39:11:66 | ControlFlowNode for Attribute() | FlaskTemplate.py:1:26:1:32 | ControlFlowNode for ImportMember | FlaskTemplate.py:11:39:11:66 | ControlFlowNode for Attribute() | This Template depends on $@. | FlaskTemplate.py:1:26:1:32 | ControlFlowNode for ImportMember | user-provided value |
-| FlaskTemplate.py:17:41:17:68 | ControlFlowNode for Attribute() | FlaskTemplate.py:1:26:1:32 | ControlFlowNode for ImportMember | FlaskTemplate.py:17:41:17:68 | ControlFlowNode for Attribute() | This Template depends on $@. | FlaskTemplate.py:1:26:1:32 | ControlFlowNode for ImportMember | user-provided value |
-| JinjaSsti.py:10:25:10:32 | ControlFlowNode for template | JinjaSsti.py:7:7:7:13 | ControlFlowNode for request | JinjaSsti.py:10:25:10:32 | ControlFlowNode for template | This Template depends on $@. | JinjaSsti.py:7:7:7:13 | ControlFlowNode for request | user-provided value |
-| JinjaSsti.py:20:28:20:35 | ControlFlowNode for template | JinjaSsti.py:16:7:16:13 | ControlFlowNode for request | JinjaSsti.py:20:28:20:35 | ControlFlowNode for template | This Template depends on $@. | JinjaSsti.py:16:7:16:13 | ControlFlowNode for request | user-provided value |
-| MakoSsti.py:9:27:9:34 | ControlFlowNode for template | MakoSsti.py:6:10:6:16 | ControlFlowNode for request | MakoSsti.py:9:27:9:34 | ControlFlowNode for template | This Template depends on $@. | MakoSsti.py:6:10:6:16 | ControlFlowNode for request | user-provided value |
-| TRender.py:7:24:7:31 | ControlFlowNode for template | TRender.py:5:13:5:19 | ControlFlowNode for request | TRender.py:7:24:7:31 | ControlFlowNode for template | This Template depends on $@. | TRender.py:5:13:5:19 | ControlFlowNode for request | user-provided value |
diff --git a/python/ql/test/experimental/query-tests/Security/CWE-074-TemplateInjection/TemplateInjection.qlref b/python/ql/test/experimental/query-tests/Security/CWE-074-TemplateInjection/TemplateInjection.qlref
deleted file mode 100644
index 90efec9f6360..000000000000
--- a/python/ql/test/experimental/query-tests/Security/CWE-074-TemplateInjection/TemplateInjection.qlref
+++ /dev/null
@@ -1 +0,0 @@
-experimental/Security/CWE-074/TemplateInjection.ql
From f0163894b679bc99dcc8f5dcb031b0432e12696d Mon Sep 17 00:00:00 2001
From: Joe Farebrother
Date: Thu, 21 Nov 2024 17:23:49 +0000
Subject: [PATCH 10/20] fix link in qhelp refs
---
python/ql/src/Security/CWE-074/TemplateInjection.qhelp | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/python/ql/src/Security/CWE-074/TemplateInjection.qhelp b/python/ql/src/Security/CWE-074/TemplateInjection.qhelp
index 619d834d1cfe..c3770d59cf2a 100644
--- a/python/ql/src/Security/CWE-074/TemplateInjection.qhelp
+++ b/python/ql/src/Security/CWE-074/TemplateInjection.qhelp
@@ -25,6 +25,6 @@
- Portswigger : [Server Side Template Injection](https://portswigger.net/web-security/server-side-template-injection)
+ Portswigger: Server-Side Template Injection.
From 494d779541f508f1f81dc85c06e914ea1269c805 Mon Sep 17 00:00:00 2001
From: Joe Farebrother
Date: Thu, 21 Nov 2024 17:43:36 +0000
Subject: [PATCH 11/20] Add changenote
---
python/ql/src/change-notes/2024-11-21-template-injection.md | 4 ++++
1 file changed, 4 insertions(+)
create mode 100644 python/ql/src/change-notes/2024-11-21-template-injection.md
diff --git a/python/ql/src/change-notes/2024-11-21-template-injection.md b/python/ql/src/change-notes/2024-11-21-template-injection.md
new file mode 100644
index 000000000000..a2d782f8cc07
--- /dev/null
+++ b/python/ql/src/change-notes/2024-11-21-template-injection.md
@@ -0,0 +1,4 @@
+---
+category: newQuery
+---
+* The Server Side Template Injection query (`py/template-injection`), originally contributed to the experimental query pack by @porcupineyhairs, has been promoted to the ain query suite. This query finds instances of templates for a template engine such as Jinja being constructed with user input.
\ No newline at end of file
From 0f0c1e1609c34c100c05dfe0c452f6a407aff159 Mon Sep 17 00:00:00 2001
From: Joe Farebrother
Date: Fri, 22 Nov 2024 16:32:10 +0000
Subject: [PATCH 12/20] Test update
---
.../library-tests/frameworks/django-v2-v3/template_test.py | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/python/ql/test/library-tests/frameworks/django-v2-v3/template_test.py b/python/ql/test/library-tests/frameworks/django-v2-v3/template_test.py
index 2d25848fde65..ba98c8f41964 100644
--- a/python/ql/test/library-tests/frameworks/django-v2-v3/template_test.py
+++ b/python/ql/test/library-tests/frameworks/django-v2-v3/template_test.py
@@ -1,13 +1,13 @@
from django.template import Template, engines
from django.urls import path
-from django.http.response import HttpResponse,
+from django.http.response import HttpResponse
def a(request): # $requestHandler
t = Template("abc").render() # $templateConstruction="abc"
return HttpResponse(t) # $HttpResponse
def b(request): # $requestHandler
- # This case is not yet supported
+ # This case is not currently supported
t = django.template.engines["django"].from_string("abc") # $MISSING:templateConstruction="abc"
return HttpResponse(t) # $HttpResponse
From 6e16ed52e886979450264578a9b21835391e6d66 Mon Sep 17 00:00:00 2001
From: Joe Farebrother
Date: Fri, 29 Nov 2024 09:49:31 +0000
Subject: [PATCH 13/20] Reveiw suggestions: Spelling/grammar fixes
Co-authored-by: Taus
---
python/ql/src/Security/CWE-074/TemplateInjection.qhelp | 2 +-
python/ql/src/Security/CWE-074/TemplateInjection.ql | 2 +-
python/ql/src/change-notes/2024-11-21-template-injection.md | 2 +-
3 files changed, 3 insertions(+), 3 deletions(-)
diff --git a/python/ql/src/Security/CWE-074/TemplateInjection.qhelp b/python/ql/src/Security/CWE-074/TemplateInjection.qhelp
index c3770d59cf2a..5b8827b05f3d 100644
--- a/python/ql/src/Security/CWE-074/TemplateInjection.qhelp
+++ b/python/ql/src/Security/CWE-074/TemplateInjection.qhelp
@@ -18,7 +18,7 @@
The following is an example of a string that could be used to cause remote code execution when interpreted as a template:
- In the following case, user input is not used to construct the template; rather is only used for as the parameters to render the template, which is safe.
+ In the following case, user input is not used to construct the template; rather it is only used as the parameters to render the template, which is safe.
In the following case, a SandboxedEnvironment is used, preventing remote code execution.
diff --git a/python/ql/src/Security/CWE-074/TemplateInjection.ql b/python/ql/src/Security/CWE-074/TemplateInjection.ql
index 2ea68414259e..53b0a2e9b15a 100644
--- a/python/ql/src/Security/CWE-074/TemplateInjection.ql
+++ b/python/ql/src/Security/CWE-074/TemplateInjection.ql
@@ -16,5 +16,5 @@ import TemplateInjectionFlow::PathGraph
from TemplateInjectionFlow::PathNode source, TemplateInjectionFlow::PathNode sink
where TemplateInjectionFlow::flowPath(source, sink)
-select sink.getNode(), source, sink, "This Template construction depends on $@.", source.getNode(),
+select sink.getNode(), source, sink, "This template construction depends on a $@.", source.getNode(),
"user-provided value"
diff --git a/python/ql/src/change-notes/2024-11-21-template-injection.md b/python/ql/src/change-notes/2024-11-21-template-injection.md
index a2d782f8cc07..7c604e9c9936 100644
--- a/python/ql/src/change-notes/2024-11-21-template-injection.md
+++ b/python/ql/src/change-notes/2024-11-21-template-injection.md
@@ -1,4 +1,4 @@
---
category: newQuery
---
-* The Server Side Template Injection query (`py/template-injection`), originally contributed to the experimental query pack by @porcupineyhairs, has been promoted to the ain query suite. This query finds instances of templates for a template engine such as Jinja being constructed with user input.
\ No newline at end of file
+* The Server Side Template Injection query (`py/template-injection`), originally contributed to the experimental query pack by @porcupineyhairs, has been promoted to the main query suite. This query finds instances of templates for a template engine such as Jinja being constructed with user input.
\ No newline at end of file
From 55557f8dd3d5e73a959ca4ddfcc537f662380858 Mon Sep 17 00:00:00 2001
From: Joe Farebrother
Date: Fri, 29 Nov 2024 10:12:46 +0000
Subject: [PATCH 14/20] Use API graohs directly
---
.../lib/semmle/python/frameworks/Jinja2.qll | 19 +++++--------------
1 file changed, 5 insertions(+), 14 deletions(-)
diff --git a/python/ql/lib/semmle/python/frameworks/Jinja2.qll b/python/ql/lib/semmle/python/frameworks/Jinja2.qll
index 0d0a8d989211..070176077e3c 100644
--- a/python/ql/lib/semmle/python/frameworks/Jinja2.qll
+++ b/python/ql/lib/semmle/python/frameworks/Jinja2.qll
@@ -33,22 +33,13 @@ module Jinja2 {
result = ModelOutput::getATypeNode("jinja.Environment~Subclass").getASubclass*()
}
- /** Gets a reference to an instance of `jinja2.Environment`. */
- private DataFlow::TypeTrackingNode instance(DataFlow::TypeTracker t) {
- t.start() and
- result = EnvironmentClass::classRef().getACall()
- or
- exists(DataFlow::TypeTracker t2 | result = instance(t2).track(t2, t))
- }
-
- /** Gets a reference to an instance of `jinja2.Environment`. */
- DataFlow::Node instance() { instance(DataFlow::TypeTracker::end()).flowsTo(result) }
+ API::Node instance() { result = classRef().getAnInstance() }
/** A call to `jinja2.Environment.from_string`. */
- private class Jinja2FromStringConstruction extends TemplateConstruction::Range,
- DataFlow::MethodCallNode
- {
- Jinja2FromStringConstruction() { this.calls(EnvironmentClass::instance(), "from_string") }
+ private class Jinja2FromStringConstruction extends TemplateConstruction::Range, API::CallNode {
+ Jinja2FromStringConstruction() {
+ this = EnvironmentClass::instance().getMember("from_string").getACall()
+ }
override DataFlow::Node getSourceArg() { result = this.getArg(0) }
}
From dd8b7a4a8fa3e26a3f7d61ce612574d6709f922d Mon Sep 17 00:00:00 2001
From: Joe Farebrother
Date: Fri, 29 Nov 2024 10:21:13 +0000
Subject: [PATCH 15/20] Add additional test for safe case in documentation
---
.../ql/test/library-tests/frameworks/jinja2/template_test.py | 4 ++++
1 file changed, 4 insertions(+)
diff --git a/python/ql/test/library-tests/frameworks/jinja2/template_test.py b/python/ql/test/library-tests/frameworks/jinja2/template_test.py
index 587de84f6216..23cc9f151b9a 100644
--- a/python/ql/test/library-tests/frameworks/jinja2/template_test.py
+++ b/python/ql/test/library-tests/frameworks/jinja2/template_test.py
@@ -1,7 +1,11 @@
from jinja2 import Environment, Template
+from jinja2.sandbox import SandboxedEnvironment
def test():
env = Environment()
t = env.from_string("abc") # $ templateConstruction="abc"
t = Template("abc") # $ templateConstruction="abc"
+
+ env2 = SandboxedEnvironment()
+ t = env.from_string("abc") # No result as we don't model SandboxedEnvironment. We may wish to instead specifically model it as NOT vulnerable to template injection vulnerabilities.
return t
\ No newline at end of file
From ebaab89933aaa6d2b95672d54ee6e8c04602a110 Mon Sep 17 00:00:00 2001
From: Joe Farebrother
Date: Fri, 29 Nov 2024 10:29:37 +0000
Subject: [PATCH 16/20] Formatting updates
---
python/ql/lib/semmle/python/Frameworks.qll | 1 -
python/ql/src/Security/CWE-074/TemplateInjection.ql | 4 ++--
.../frameworks/django-v2-v3/template_test.py | 12 ++++++------
.../library-tests/frameworks/flask/template_test.py | 4 ++--
.../library-tests/frameworks/jinja2/template_test.py | 2 +-
5 files changed, 11 insertions(+), 12 deletions(-)
diff --git a/python/ql/lib/semmle/python/Frameworks.qll b/python/ql/lib/semmle/python/Frameworks.qll
index af9417308ab3..e6af222a615f 100644
--- a/python/ql/lib/semmle/python/Frameworks.qll
+++ b/python/ql/lib/semmle/python/Frameworks.qll
@@ -17,7 +17,6 @@ private import semmle.python.frameworks.Asyncpg
private import semmle.python.frameworks.Baize
private import semmle.python.frameworks.Bottle
private import semmle.python.frameworks.BSon
-private import semmle.python.frameworks.Bottle
private import semmle.python.frameworks.CassandraDriver
private import semmle.python.frameworks.Chameleon
private import semmle.python.frameworks.Cherrypy
diff --git a/python/ql/src/Security/CWE-074/TemplateInjection.ql b/python/ql/src/Security/CWE-074/TemplateInjection.ql
index 53b0a2e9b15a..bc4c935bc377 100644
--- a/python/ql/src/Security/CWE-074/TemplateInjection.ql
+++ b/python/ql/src/Security/CWE-074/TemplateInjection.ql
@@ -16,5 +16,5 @@ import TemplateInjectionFlow::PathGraph
from TemplateInjectionFlow::PathNode source, TemplateInjectionFlow::PathNode sink
where TemplateInjectionFlow::flowPath(source, sink)
-select sink.getNode(), source, sink, "This template construction depends on a $@.", source.getNode(),
- "user-provided value"
+select sink.getNode(), source, sink, "This template construction depends on a $@.",
+ source.getNode(), "user-provided value"
diff --git a/python/ql/test/library-tests/frameworks/django-v2-v3/template_test.py b/python/ql/test/library-tests/frameworks/django-v2-v3/template_test.py
index ba98c8f41964..1f59b4a03c23 100644
--- a/python/ql/test/library-tests/frameworks/django-v2-v3/template_test.py
+++ b/python/ql/test/library-tests/frameworks/django-v2-v3/template_test.py
@@ -2,14 +2,14 @@
from django.urls import path
from django.http.response import HttpResponse
-def a(request): # $requestHandler
- t = Template("abc").render() # $templateConstruction="abc"
- return HttpResponse(t) # $HttpResponse
+def a(request): # $ requestHandler
+ t = Template("abc").render() # $ templateConstruction="abc"
+ return HttpResponse(t) # $ HttpResponse
-def b(request): # $requestHandler
+def b(request): # $ requestHandler
# This case is not currently supported
- t = django.template.engines["django"].from_string("abc") # $MISSING:templateConstruction="abc"
- return HttpResponse(t) # $HttpResponse
+ t = django.template.engines["django"].from_string("abc") # $ MISSING:templateConstruction="abc"
+ return HttpResponse(t) # $ HttpResponse
urlpatterns = [
path("a", a), # $ routeSetup="a"
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 8d867e148291..e50ab7063550 100644
--- a/python/ql/test/library-tests/frameworks/flask/template_test.py
+++ b/python/ql/test/library-tests/frameworks/flask/template_test.py
@@ -2,12 +2,12 @@
app = Flask(__name__)
@app.route("/a") # $routeSetup="/a"
-def a(): # $requestHandler
+def a(): # $ requestHandler
r = render_template_string("abc") # $ templateConstruction="abc"
return r # $ HttpResponse
@app.route("/b") # $routeSetup="/b"
-def b(): # $requestHandler
+def b(): # $ requestHandler
s = stream_template_string("abc") # $ templateConstruction="abc"
r = Response(stream_with_context(s)) # $ HttpResponse
return r # $ HttpResponse
diff --git a/python/ql/test/library-tests/frameworks/jinja2/template_test.py b/python/ql/test/library-tests/frameworks/jinja2/template_test.py
index 23cc9f151b9a..40004b4f1c50 100644
--- a/python/ql/test/library-tests/frameworks/jinja2/template_test.py
+++ b/python/ql/test/library-tests/frameworks/jinja2/template_test.py
@@ -7,5 +7,5 @@ def test():
t = Template("abc") # $ templateConstruction="abc"
env2 = SandboxedEnvironment()
- t = env.from_string("abc") # No result as we don't model SandboxedEnvironment. We may wish to instead specifically model it as NOT vulnerable to template injection vulnerabilities.
+ t = env2.from_string("abc") # No result as we don't model SandboxedEnvironment. We may wish to instead specifically model it as NOT vulnerable to template injection vulnerabilities.
return t
\ No newline at end of file
From ef1d898b0d86bf0d2982ca6a65b54a9ff29bf0a0 Mon Sep 17 00:00:00 2001
From: Joe Farebrother
Date: Fri, 29 Nov 2024 10:44:48 +0000
Subject: [PATCH 17/20] Add qldoc
---
python/ql/lib/semmle/python/frameworks/Jinja2.qll | 1 +
1 file changed, 1 insertion(+)
diff --git a/python/ql/lib/semmle/python/frameworks/Jinja2.qll b/python/ql/lib/semmle/python/frameworks/Jinja2.qll
index 070176077e3c..0387db936311 100644
--- a/python/ql/lib/semmle/python/frameworks/Jinja2.qll
+++ b/python/ql/lib/semmle/python/frameworks/Jinja2.qll
@@ -33,6 +33,7 @@ module Jinja2 {
result = ModelOutput::getATypeNode("jinja.Environment~Subclass").getASubclass*()
}
+ /** Gets a reference to an instance of `jinja2.Environment`. */
API::Node instance() { result = classRef().getAnInstance() }
/** A call to `jinja2.Environment.from_string`. */
From 462be46be9897ca9a7bd99f06d52eafb2e288bc6 Mon Sep 17 00:00:00 2001
From: Joe Farebrother
Date: Fri, 29 Nov 2024 20:54:03 +0000
Subject: [PATCH 18/20] Update test output
---
.../CWE-074-TemplateInjection/TemplateInjection.expected | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/python/ql/test/query-tests/Security/CWE-074-TemplateInjection/TemplateInjection.expected b/python/ql/test/query-tests/Security/CWE-074-TemplateInjection/TemplateInjection.expected
index 3a833787a982..f92107728395 100644
--- a/python/ql/test/query-tests/Security/CWE-074-TemplateInjection/TemplateInjection.expected
+++ b/python/ql/test/query-tests/Security/CWE-074-TemplateInjection/TemplateInjection.expected
@@ -12,5 +12,5 @@ nodes
| JinjaSsti.py:21:25:21:32 | ControlFlowNode for template | semmle.label | ControlFlowNode for template |
subpaths
#select
-| JinjaSsti.py:10:18:10:25 | ControlFlowNode for template | JinjaSsti.py:7:7:7:13 | ControlFlowNode for request | JinjaSsti.py:10:18:10:25 | ControlFlowNode for template | This Template construction depends on $@. | JinjaSsti.py:7:7:7:13 | ControlFlowNode for request | user-provided value |
-| JinjaSsti.py:21:25:21:32 | ControlFlowNode for template | JinjaSsti.py:16:7:16:13 | ControlFlowNode for request | JinjaSsti.py:21:25:21:32 | ControlFlowNode for template | This Template construction depends on $@. | JinjaSsti.py:16:7:16:13 | ControlFlowNode for request | user-provided value |
+| JinjaSsti.py:10:18:10:25 | ControlFlowNode for template | JinjaSsti.py:7:7:7:13 | ControlFlowNode for request | JinjaSsti.py:10:18:10:25 | ControlFlowNode for template | This template construction depends on a $@. | JinjaSsti.py:7:7:7:13 | ControlFlowNode for request | user-provided value |
+| JinjaSsti.py:21:25:21:32 | ControlFlowNode for template | JinjaSsti.py:16:7:16:13 | ControlFlowNode for request | JinjaSsti.py:21:25:21:32 | ControlFlowNode for template | This template construction depends on a $@. | JinjaSsti.py:16:7:16:13 | ControlFlowNode for request | user-provided value |
From 8a778da25327d1fa6ebe62adcd326289cbc11871 Mon Sep 17 00:00:00 2001
From: Joe Farebrother
Date: Mon, 9 Dec 2024 10:22:24 +0000
Subject: [PATCH 19/20] Apply suggestions from docs review
Co-authored-by: Ben Ahmady <32935794+subatoi@users.noreply.github.com>
---
python/ql/src/Security/CWE-074/TemplateInjection.qhelp | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/python/ql/src/Security/CWE-074/TemplateInjection.qhelp b/python/ql/src/Security/CWE-074/TemplateInjection.qhelp
index 5b8827b05f3d..ee416b77eec9 100644
--- a/python/ql/src/Security/CWE-074/TemplateInjection.qhelp
+++ b/python/ql/src/Security/CWE-074/TemplateInjection.qhelp
@@ -8,7 +8,7 @@
Ensure that an untrusted value is not used to directly construct a template.
- Jinja also provides a SandboxedEnvironment that prohibits access to unsafe methods and attributes, that can be used if constructing a template from user input is absolutely necessary.
+ Jinja also provides SandboxedEnvironment that prohibits access to unsafe methods and attributes. This can be used if constructing a template from user input is absolutely necessary.
@@ -18,7 +18,7 @@
The following is an example of a string that could be used to cause remote code execution when interpreted as a template:
- In the following case, user input is not used to construct the template; rather it is only used as the parameters to render the template, which is safe.
+ In the following case, user input is not used to construct the template. Instead, it is only used as the parameters to render the template, which is safe.
In the following case, a SandboxedEnvironment is used, preventing remote code execution.
From f82fa202496071d8587a6c6b96b836dd55813061 Mon Sep 17 00:00:00 2001
From: Joe Farebrother
Date: Mon, 9 Dec 2024 20:37:11 +0000
Subject: [PATCH 20/20] Update test outputs
---
.../test/library-tests/frameworks/Genshi/ConceptsTest.expected | 2 --
.../ql/test/library-tests/frameworks/Mako/ConceptsTest.expected | 2 --
.../test/library-tests/frameworks/TRender/ConceptsTest.expected | 2 --
.../library-tests/frameworks/airspeed/ConceptsTest.expected | 2 --
.../library-tests/frameworks/chameleon/ConceptsTest.expected | 2 --
.../test/library-tests/frameworks/chevron/ConceptsTest.expected | 2 --
.../test/library-tests/frameworks/jinja2/ConceptsTest.expected | 2 --
7 files changed, 14 deletions(-)
diff --git a/python/ql/test/library-tests/frameworks/Genshi/ConceptsTest.expected b/python/ql/test/library-tests/frameworks/Genshi/ConceptsTest.expected
index a74f2c23cda2..e69de29bb2d1 100644
--- a/python/ql/test/library-tests/frameworks/Genshi/ConceptsTest.expected
+++ b/python/ql/test/library-tests/frameworks/Genshi/ConceptsTest.expected
@@ -1,2 +0,0 @@
-testFailures
-failures
\ No newline at end of file
diff --git a/python/ql/test/library-tests/frameworks/Mako/ConceptsTest.expected b/python/ql/test/library-tests/frameworks/Mako/ConceptsTest.expected
index a74f2c23cda2..e69de29bb2d1 100644
--- a/python/ql/test/library-tests/frameworks/Mako/ConceptsTest.expected
+++ b/python/ql/test/library-tests/frameworks/Mako/ConceptsTest.expected
@@ -1,2 +0,0 @@
-testFailures
-failures
\ No newline at end of file
diff --git a/python/ql/test/library-tests/frameworks/TRender/ConceptsTest.expected b/python/ql/test/library-tests/frameworks/TRender/ConceptsTest.expected
index a74f2c23cda2..e69de29bb2d1 100644
--- a/python/ql/test/library-tests/frameworks/TRender/ConceptsTest.expected
+++ b/python/ql/test/library-tests/frameworks/TRender/ConceptsTest.expected
@@ -1,2 +0,0 @@
-testFailures
-failures
\ No newline at end of file
diff --git a/python/ql/test/library-tests/frameworks/airspeed/ConceptsTest.expected b/python/ql/test/library-tests/frameworks/airspeed/ConceptsTest.expected
index a74f2c23cda2..e69de29bb2d1 100644
--- a/python/ql/test/library-tests/frameworks/airspeed/ConceptsTest.expected
+++ b/python/ql/test/library-tests/frameworks/airspeed/ConceptsTest.expected
@@ -1,2 +0,0 @@
-testFailures
-failures
\ No newline at end of file
diff --git a/python/ql/test/library-tests/frameworks/chameleon/ConceptsTest.expected b/python/ql/test/library-tests/frameworks/chameleon/ConceptsTest.expected
index a74f2c23cda2..e69de29bb2d1 100644
--- a/python/ql/test/library-tests/frameworks/chameleon/ConceptsTest.expected
+++ b/python/ql/test/library-tests/frameworks/chameleon/ConceptsTest.expected
@@ -1,2 +0,0 @@
-testFailures
-failures
\ No newline at end of file
diff --git a/python/ql/test/library-tests/frameworks/chevron/ConceptsTest.expected b/python/ql/test/library-tests/frameworks/chevron/ConceptsTest.expected
index a74f2c23cda2..e69de29bb2d1 100644
--- a/python/ql/test/library-tests/frameworks/chevron/ConceptsTest.expected
+++ b/python/ql/test/library-tests/frameworks/chevron/ConceptsTest.expected
@@ -1,2 +0,0 @@
-testFailures
-failures
\ No newline at end of file
diff --git a/python/ql/test/library-tests/frameworks/jinja2/ConceptsTest.expected b/python/ql/test/library-tests/frameworks/jinja2/ConceptsTest.expected
index a74f2c23cda2..e69de29bb2d1 100644
--- a/python/ql/test/library-tests/frameworks/jinja2/ConceptsTest.expected
+++ b/python/ql/test/library-tests/frameworks/jinja2/ConceptsTest.expected
@@ -1,2 +0,0 @@
-testFailures
-failures
\ No newline at end of file