Skip to content

Commit f40242d

Browse files
authored
Merge pull request github#3396 from porcupineyhairs/python-ssti
Python : Add query to detect Server Side Template Injection
2 parents f5c1de8 + 7a71ca3 commit f40242d

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

66 files changed

+1004
-0
lines changed
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
from django.urls import path
2+
from django.http import HttpResponse
3+
from jinja2 import Template as Jinja2_Template
4+
from jinja2 import Environment, DictLoader, escape
5+
6+
7+
def a(request):
8+
# Load the template
9+
template = request.GET['template']
10+
t = Jinja2_Template(template)
11+
name = request.GET['name']
12+
# Render the template with the context data
13+
html = t.render(name=escape(name))
14+
return HttpResponse(html)
15+
16+
17+
urlpatterns = [
18+
path('a', a),
19+
]
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
from django.urls import path
2+
from django.http import HttpResponse
3+
from jinja2 import Template as Jinja2_Template
4+
from jinja2 import Environment, DictLoader, escape
5+
6+
7+
def a(request):
8+
# Load the template
9+
template = request.GET['template']
10+
env = SandboxedEnvironment(undefined=StrictUndefined)
11+
t = env.from_string(template)
12+
name = request.GET['name']
13+
# Render the template with the context data
14+
html = t.render(name=escape(name))
15+
return HttpResponse(html)
16+
17+
18+
urlpatterns = [
19+
path('a', a),
20+
]
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
<!DOCTYPE qhelp SYSTEM "qhelp.dtd">
2+
<qhelp>
3+
<overview>
4+
<p>
5+
Template Injection occurs when user input is embedded in a template in an unsafe manner.
6+
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.
7+
</p>
8+
</overview>
9+
<recommendation>
10+
<p>
11+
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.
12+
</p>
13+
</recommendation>
14+
<example>
15+
<p>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. </p>
16+
<sample src="JinjaBad.py" />
17+
18+
<p>Here we have fixed the problem by using the Jinja sandbox environment for evaluating untrusted code.</p>
19+
<sample src="JinjaGood.py" />
20+
</example>
21+
<references>
22+
<li>Portswigger : [Server Side Template Injection](https://portswigger.net/web-security/server-side-template-injection)</li>
23+
</references>
24+
</qhelp>
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
/**
2+
* @name Server Side Template Injection
3+
* @description Using user-controlled data to create a template can cause security issues.
4+
* @kind path-problem
5+
* @problem.severity error
6+
* @precision high
7+
* @id py/template-injection
8+
* @tags security
9+
* external/cwe/cwe-074
10+
*/
11+
12+
import python
13+
import semmle.python.security.Paths
14+
/* Sources */
15+
import semmle.python.web.HttpRequest
16+
/* Sinks */
17+
import experimental.semmle.python.templates.Ssti
18+
/* Flow */
19+
import semmle.python.security.strings.Untrusted
20+
21+
class TemplateInjectionConfiguration extends TaintTracking::Configuration {
22+
TemplateInjectionConfiguration() { this = "Template injection configuration" }
23+
24+
override predicate isSource(TaintTracking::Source source) {
25+
source instanceof HttpRequestTaintSource
26+
}
27+
28+
override predicate isSink(TaintTracking::Sink sink) { sink instanceof SSTISink }
29+
}
30+
31+
from TemplateInjectionConfiguration config, TaintedPathSource src, TaintedPathSink sink
32+
where config.hasFlowPath(src, sink)
33+
select sink.getSink(), src, sink, "This Template depends on $@.", src.getSource(),
34+
"a user-provided value"
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
/** Provides classes which model the `airspeed` package. */
2+
3+
import python
4+
import semmle.python.web.HttpRequest
5+
import experimental.semmle.python.templates.SSTISink
6+
7+
/** returns the ClassValue representing `airspeed.Template` */
8+
ClassValue theAirspeedTemplateClass() { result = Value::named("airspeed.Template") }
9+
10+
/**
11+
* Sink representing the `airspeed.Template` class instantiation argument.
12+
*
13+
* import airspeed
14+
* temp = airspeed.Template(`"sink"`)
15+
*/
16+
class AirspeedTemplateSink extends SSTISink {
17+
override string toString() { result = "argument to airspeed.Template()" }
18+
19+
AirspeedTemplateSink() {
20+
exists(CallNode call |
21+
call.getFunction().pointsTo(theAirspeedTemplateClass()) and
22+
call.getArg(0) = this
23+
)
24+
}
25+
26+
override predicate sinks(TaintKind kind) { kind instanceof ExternalStringKind }
27+
}
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
/** Provides classes which model the `bottle` package. */
2+
3+
import python
4+
import semmle.python.web.HttpRequest
5+
import experimental.semmle.python.templates.SSTISink
6+
7+
/** returns the ClassValue representing `bottle.SimpleTemplate` */
8+
ClassValue theBottleSimpleTemplateClass() { result = Value::named("bottle.SimpleTemplate") }
9+
10+
/**
11+
* Sink representing the `bottle.SimpleTemplate` class instantiation argument.
12+
*
13+
* from bottle import SimpleTemplate
14+
* template = SimpleTemplate(`sink`)
15+
*/
16+
class BottleSimpleTemplateSink extends SSTISink {
17+
override string toString() { result = "argument to bottle.SimpleTemplate()" }
18+
19+
BottleSimpleTemplateSink() {
20+
exists(CallNode call |
21+
call.getFunction().pointsTo(theBottleSimpleTemplateClass()) and
22+
call.getArg(0) = this
23+
)
24+
}
25+
26+
override predicate sinks(TaintKind kind) { kind instanceof ExternalStringKind }
27+
}
28+
29+
/**
30+
* Sink representing the `bottle.template` function call argument.
31+
*
32+
* from bottle import template
33+
* tmp = template(`sink`)
34+
*/
35+
class BottleTemplateSink extends SSTISink {
36+
override string toString() { result = "argument to bottle.template()" }
37+
38+
BottleTemplateSink() {
39+
exists(CallNode call |
40+
call.getFunction() = theBottleModule().attr("template").getAReference() and
41+
call.getArg(0) = this
42+
)
43+
}
44+
45+
override predicate sinks(TaintKind kind) { kind instanceof ExternalStringKind }
46+
}
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
/** Provides classes which model the `Chameleon` package. */
2+
3+
import python
4+
import semmle.python.web.HttpRequest
5+
import experimental.semmle.python.templates.SSTISink
6+
7+
/** returns the ClassValue representing `chameleon.PageTemplate` */
8+
ClassValue theChameleonPageTemplateClass() { result = Value::named("chameleon.PageTemplate") }
9+
10+
/**
11+
* Sink representing the `chameleon.PageTemplate` class instantiation argument.
12+
*
13+
* from chameleon import PageTemplate
14+
* template = PageTemplate(`sink`)
15+
*/
16+
class ChameleonTemplateSink extends SSTISink {
17+
override string toString() { result = "argument to Chameleon.PageTemplate()" }
18+
19+
ChameleonTemplateSink() {
20+
exists(CallNode call |
21+
call.getFunction().pointsTo(theChameleonPageTemplateClass()) and
22+
call.getArg(0) = this
23+
)
24+
}
25+
26+
override predicate sinks(TaintKind kind) { kind instanceof ExternalStringKind }
27+
}
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
/** Provides classes which model the `Cheetah3` package. */
2+
3+
import python
4+
import semmle.python.web.HttpRequest
5+
import experimental.semmle.python.templates.SSTISink
6+
7+
/** returns the ClassValue representing `Cheetah.Template.Template` */
8+
ClassValue theCheetahTemplateClass() { result = Value::named("Cheetah.Template.Template") }
9+
10+
/**
11+
* Sink representing the instantiation argument of any class which derives from
12+
* the `Cheetah.Template.Template` class .
13+
*
14+
* from Cheetah.Template import Template
15+
* class Template3(Template):
16+
* title = 'Hello World Example!'
17+
* contents = 'Hello World!'
18+
* t3 = Template3("sink")
19+
*
20+
* This will also detect cases of the following type :
21+
*
22+
* from Cheetah.Template import Template
23+
* t3 = Template("sink")
24+
*/
25+
class CheetahTemplateInstantiationSink extends SSTISink {
26+
override string toString() { result = "argument to Cheetah.Template.Template()" }
27+
28+
CheetahTemplateInstantiationSink() {
29+
exists(CallNode call, ClassValue cv |
30+
cv.getASuperType() = theCheetahTemplateClass() and
31+
call.getFunction().pointsTo(cv) and
32+
call.getArg(0) = this
33+
)
34+
}
35+
36+
override predicate sinks(TaintKind kind) { kind instanceof ExternalStringKind }
37+
}
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
/** Provides classes which model the `chevron` package. */
2+
3+
import python
4+
import semmle.python.web.HttpRequest
5+
import experimental.semmle.python.templates.SSTISink
6+
7+
/** returns the Value representing `chevron.render` function */
8+
Value theChevronRenderFunc() { result = Value::named("chevron.render") }
9+
10+
/**
11+
* Sink representing the `chevron.render` function call argument.
12+
*
13+
* import chevron
14+
* tmp = chevron.render(`sink`,{ 'key' : 'value' })
15+
*/
16+
class ChevronRenderSink extends SSTISink {
17+
override string toString() { result = "argument to chevron.render()" }
18+
19+
ChevronRenderSink() {
20+
exists(CallNode call |
21+
call.getFunction() = theChevronRenderFunc().getAReference() and
22+
call.getArg(0) = this
23+
)
24+
// TODO: this should also detect :
25+
// import chevron
26+
// args = {
27+
// 'template': 'sink',
28+
// 'data': {
29+
// 'mustache': 'World'
30+
// }
31+
// }
32+
// chevron.render(**args)
33+
}
34+
35+
override predicate sinks(TaintKind kind) { kind instanceof ExternalStringKind }
36+
}
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
/** Provides classes which model the `DjangoTemplate` package. */
2+
3+
import python
4+
import semmle.python.web.HttpRequest
5+
import experimental.semmle.python.templates.SSTISink
6+
7+
ClassValue theDjangoTemplateClass() { result = Value::named("django.template.Template") }
8+
9+
/**
10+
* Sink representng `django.template.Template` class instantiation argument.
11+
*
12+
* from django.template import Template
13+
* template = Template(`sink`)
14+
*/
15+
class DjangoTemplateTemplateSink extends SSTISink {
16+
override string toString() { result = "argument to Django.template()" }
17+
18+
DjangoTemplateTemplateSink() {
19+
exists(CallNode call |
20+
call.getFunction().pointsTo(theDjangoTemplateClass()) and
21+
call.getArg(0) = this
22+
)
23+
}
24+
25+
override predicate sinks(TaintKind kind) { kind instanceof ExternalStringKind }
26+
}
27+
// TODO (intentionally commented out QLDoc, since qlformat will delete those lines otherwise)
28+
// /**
29+
// * Sinks representng the django.template.Template class instantiation.
30+
// *
31+
// * from django.template import engines
32+
// *
33+
// * django_engine = engines["django"]
34+
// * template = django_engine.from_string(`sink`)
35+
// */

0 commit comments

Comments
 (0)