Skip to content

Commit c75fa28

Browse files
authored
Merge pull request #538 from markshannon/python-jinja2-autoescape
Python: New query to check for use of jinja2 templates without auto-escaping
2 parents 57a976d + 21246dc commit c75fa28

File tree

9 files changed

+206
-3
lines changed

9 files changed

+206
-3
lines changed

change-notes/1.19/analysis-python.md

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -56,9 +56,10 @@ A new predicate `Stmt.getAnEntryNode()` has been added to make it easier to writ
5656

5757
| **Query** | **Tags** | **Purpose** |
5858
|-----------------------------|-----------|--------------------------------------------------------------------|
59-
| Flask app is run in debug mode (`py/flask-debug`) | security, external/cwe/cwe-215, external/cwe/cwe-489 | Finds instances where a Flask application is run in debug mode. Enabled on LGTM by default. |
60-
| Information exposure through an exception (`py/stack-trace-exposure`) | security, external/cwe/cwe-209, external/cwe/cwe-497 | Finds instances where information about an exception may be leaked to an external user. Enabled on LGTM by default. |
61-
| Request without certificate validation (`py/request-without-cert-validation`) | security, external/cwe/cwe-295 | Finds requests where certificate verification has been explicitly turned off, possibly allowing man-in-the-middle attacks. Not enabled on LGTM by default. |
59+
| Flask app is run in debug mode (`py/flask-debug`) | security, external/cwe/cwe-215, external/cwe/cwe-489 | Finds instances where a Flask application is run in debug mode. Results are shown on LGTM by default. |
60+
| Information exposure through an exception (`py/stack-trace-exposure`) | security, external/cwe/cwe-209, external/cwe/cwe-497 | Finds instances where information about an exception may be leaked to an external user. Results are shown on LGTM by default. |
61+
| Jinja2 templating with autoescape=False (`py/jinja2/autoescape-false`) | security, external/cwe/cwe-079 | Finds instantiations of `jinja2.Environment` with `autoescape=False` which may allow XSS attacks. Results are hidden on LGTM by default. |
62+
| Request without certificate validation (`py/request-without-cert-validation`) | security, external/cwe/cwe-295 | Finds requests where certificate verification has been explicitly turned off, possibly allowing man-in-the-middle attacks. Results are hidden on LGTM by default. |
6263

6364
## Changes to existing queries
6465

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
<!DOCTYPE qhelp PUBLIC
2+
"-//Semmle//qhelp//EN"
3+
"qhelp.dtd">
4+
<qhelp>
5+
6+
<overview>
7+
<p>
8+
9+
Cross-site scripting (XSS) attacks can occur if untrusted input is not escaped. This applies to templates as well as code.
10+
The <code>jinja2</code> templates may be vulnerable to XSS if the environment has <code>autoescape</code> set to <code>False</code>.
11+
Unfortunately, <code>jinja2</code> sets <code>autoescape</code> to <code>False</code> by default.
12+
Explicitly setting <code>autoescape</code> to <code>True</code> when creating an <code>Environment</code> object will prevent this.
13+
</p>
14+
</overview>
15+
16+
<recommendation>
17+
<p>
18+
Avoid setting jinja2 autoescape to False.
19+
Jinja2 provides the function <code>select_autoescape</code> to make sure that the correct auto-escaping is chosen.
20+
For example, it can be used when creating an environment <code>Environment(autoescape=select_autoescape(['html', 'xml'])</code>
21+
</p>
22+
</recommendation>
23+
24+
<example>
25+
<p>
26+
The following example is a minimal Flask app which shows a safe and an unsafe way to render the given name back to the page.
27+
The first view is unsafe as <code>first_name</code> is not escaped, leaving the page vulnerable to cross-site scripting attacks.
28+
The second view is safe as <code>first_name</code> is escaped, so it is not vulnerable to cross-site scripting attacks.
29+
</p>
30+
<sample src="examples/jinja2.py" />
31+
</example>
32+
33+
<references>
34+
<li>
35+
Jinja2: <a href="http://jinja.pocoo.org/docs/2.10/api/">API</a>.
36+
</li>
37+
<li>
38+
Wikipedia: <a href="http://en.wikipedia.org/wiki/Cross-site_scripting">Cross-site scripting</a>.
39+
</li>
40+
<li>
41+
OWASP: <a href="https://www.owasp.org/index.php/XSS_(Cross_Site_Scripting)_Prevention_Cheat_Sheet">XSS (Cross Site Scripting) Prevention Cheat Sheet</a>.
42+
</li>
43+
</references>
44+
</qhelp>
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
/**
2+
* @name Jinja2 templating with autoescape=False
3+
* @description Using jinja2 templates with 'autoescape=False' can
4+
* cause a cross-site scripting vulnerability.
5+
* @kind problem
6+
* @problem.severity error
7+
* @precision medium
8+
* @id py/jinja2/autoescape-false
9+
* @tags security
10+
* external/cwe/cwe-079
11+
*/
12+
13+
import python
14+
15+
ClassObject jinja2EnvironmentOrTemplate() {
16+
exists(ModuleObject jinja2, string name |
17+
jinja2.getName() = "jinja2" and
18+
jinja2.getAttribute(name) = result |
19+
name = "Environment" or
20+
name = "Template"
21+
)
22+
}
23+
24+
ControlFlowNode getAutoEscapeParameter(CallNode call) {
25+
exists(Object callable |
26+
call.getFunction().refersTo(callable) |
27+
callable = jinja2EnvironmentOrTemplate() and
28+
result = call.getArgByName("autoescape")
29+
)
30+
}
31+
32+
from CallNode call
33+
where
34+
not exists(call.getNode().getStarargs()) and
35+
not exists(call.getNode().getKwargs()) and
36+
(
37+
not exists(getAutoEscapeParameter(call)) and
38+
exists(Object env |
39+
call.getFunction().refersTo(env) and
40+
env = jinja2EnvironmentOrTemplate()
41+
)
42+
or
43+
exists(Object isFalse |
44+
getAutoEscapeParameter(call).refersTo(isFalse) and isFalse.booleanValue() = false
45+
)
46+
)
47+
48+
select call, "Using jinja2 templates with autoescape=False can potentially allow XSS attacks."
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
from flask import Flask, request, make_response, escape
2+
from jinja2 import Environment, select_autoescape, FileSystemLoader
3+
4+
app = Flask(__name__)
5+
loader = FileSystemLoader( searchpath="templates/" )
6+
7+
unsafe_env = Environment(loader=loader)
8+
safe1_env = Environment(loader=loader, autoescape=True)
9+
safe2_env = Environment(loader=loader, autoescape=select_autoescape())
10+
11+
def render_response_from_env(env):
12+
name = request.args.get('name', '')
13+
template = env.get_template('template.html')
14+
return make_response(template.render(name=name))
15+
16+
@app.route('/unsafe')
17+
def unsafe():
18+
return render_response_from_env(unsafe_env)
19+
20+
@app.route('/safe1')
21+
def safe1():
22+
return render_response_from_env(safe1_env)
23+
24+
@app.route('/safe2')
25+
def safe2():
26+
return render_response_from_env(safe2_env)
27+
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
| jinja2_escaping.py:9:14:9:39 | ControlFlowNode for Environment() | Using jinja2 templates with autoescape=False can potentially allow XSS attacks. |
2+
| jinja2_escaping.py:41:5:41:29 | ControlFlowNode for Environment() | Using jinja2 templates with autoescape=False can potentially allow XSS attacks. |
3+
| jinja2_escaping.py:43:1:43:3 | ControlFlowNode for E() | Using jinja2 templates with autoescape=False can potentially allow XSS attacks. |
4+
| jinja2_escaping.py:44:1:44:15 | ControlFlowNode for E() | Using jinja2 templates with autoescape=False can potentially allow XSS attacks. |
5+
| jinja2_escaping.py:53:15:53:43 | ControlFlowNode for Template() | Using jinja2 templates with autoescape=False can potentially allow XSS attacks. |
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Security/CWE-079/Jinja2WithoutEscaping.ql

python/ql/test/query-tests/Security/CWE-079/ReflectedXss.expected

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
edges
22
| ../lib/flask/__init__.py:14:19:14:20 | externally controlled string | ../lib/flask/__init__.py:15:19:15:20 | externally controlled string |
33
| ../lib/flask/__init__.py:14:19:14:20 | externally controlled string | ../lib/flask/__init__.py:16:25:16:26 | externally controlled string |
4+
| jinja2_escaping.py:14:12:14:23 | dict of externally controlled string | jinja2_escaping.py:14:12:14:39 | externally controlled string |
5+
| jinja2_escaping.py:14:12:14:39 | externally controlled string | jinja2_escaping.py:16:47:16:50 | externally controlled string |
46
| reflected_xss.py:7:18:7:29 | dict of externally controlled string | reflected_xss.py:7:18:7:45 | externally controlled string |
57
| reflected_xss.py:7:18:7:45 | externally controlled string | reflected_xss.py:8:44:8:53 | externally controlled string |
68
| reflected_xss.py:8:26:8:53 | externally controlled string | ../lib/flask/__init__.py:14:19:14:20 | externally controlled string |
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
2+
Environment(loader=templateLoader, autoescape=fake_func())
3+
from flask import Flask, request, make_response, escape
4+
from jinja2 import Environment, select_autoescape, FileSystemLoader, Template
5+
6+
app = Flask(__name__)
7+
loader = FileSystemLoader( searchpath="templates/" )
8+
9+
unsafe_env = Environment(loader=loader)
10+
safe1_env = Environment(loader=loader, autoescape=True)
11+
safe2_env = Environment(loader=loader, autoescape=select_autoescape())
12+
13+
def render_response_from_env(env):
14+
name = request.args.get('name', '')
15+
template = env.get_template('template.html')
16+
return make_response(template.render(name=name))
17+
18+
@app.route('/unsafe')
19+
def unsafe():
20+
return render_response_from_env(unsafe_env)
21+
22+
@app.route('/safe1')
23+
def safe1():
24+
return render_response_from_env(safe1_env)
25+
26+
@app.route('/safe2')
27+
def safe2():
28+
return render_response_from_env(safe2_env)
29+
30+
# Explicit autoescape
31+
32+
e = Environment(
33+
loader=loader,
34+
autoescape=select_autoescape(['html', 'htm', 'xml'])
35+
) # GOOD
36+
37+
# Additional checks with flow.
38+
auto = select_autoescape
39+
e = Environment(autoescape=auto) # GOOD
40+
z = 0
41+
e = Environment(autoescape=z) # BAD
42+
E = Environment
43+
E() # BAD
44+
E(autoescape=z) # BAD
45+
E(autoescape=auto) # GOOD
46+
E(autoescape=0+1) # GOOD
47+
48+
def checked(cond=False):
49+
if cond:
50+
e = Environment(autoescape=cond) # GOOD
51+
52+
53+
unsafe_tmpl = Template('Hello {{ name }}!')
54+
safe1_tmpl = Template('Hello {{ name }}!', autoescape=True)
55+
safe2_tmpl = Template('Hello {{ name }}!', autoescape=select_autoescape())
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
2+
class Environment(object):
3+
4+
def __init__(self, loader, autoescape):
5+
pass
6+
7+
class Template(object):
8+
9+
def __init__(self, source, autoescape):
10+
pass
11+
12+
def select_autoescape(files=[]):
13+
def autoescape(template_name):
14+
pass
15+
return autoescape
16+
17+
class FileSystemLoader(object):
18+
19+
def __init__(self, searchpath):
20+
pass

0 commit comments

Comments
 (0)