python.flask.security.audit.directly-returned-format-string.directly-returned-format-string

Author
7,347
Download Count*
License
Detected Flask route directly returning a formatted string. This is subject to cross-site scripting if user input can reach the string. Consider using the template engine instead and rendering pages with 'render_template()'.
Run Locally
Run in CI
Defintion
rules:
- id: directly-returned-format-string
message: Detected Flask route directly returning a formatted string. This is
subject to cross-site scripting if user input can reach the string.
Consider using the template engine instead and rendering pages with
'render_template()'.
metadata:
cwe:
- "CWE-79: Improper Neutralization of Input During Web Page Generation
('Cross-site Scripting')"
owasp:
- A07:2017 - Cross-Site Scripting (XSS)
- A03:2021 - Injection
category: security
technology:
- flask
references:
- https://owasp.org/Top10/A03_2021-Injection
cwe2022-top25: true
cwe2021-top25: true
subcategory:
- vuln
likelihood: HIGH
impact: MEDIUM
confidence: MEDIUM
license: Commons Clause License Condition v1.0[LGPL-2.1-only]
vulnerability_class:
- Cross-Site-Scripting (XSS)
languages:
- python
severity: WARNING
mode: taint
pattern-sources:
- pattern-either:
- patterns:
- pattern-inside: |
@$APP.route(...)
def $FUNC(..., $PARAM, ...):
...
- pattern: $PARAM
- pattern: |
request.$FUNC.get(...)
- pattern: |
request.$FUNC(...)
- pattern: request.$FUNC[...]
pattern-sinks:
- patterns:
- pattern-not-inside: return "..."
- pattern-either:
- pattern: return "...".format(...)
- pattern: return "..." % ...
- pattern: return "..." + ...
- pattern: return ... + "..."
- pattern: return f"...{...}..."
- patterns:
- pattern: return $X
- pattern-either:
- pattern-inside: |
$X = "...".format(...)
...
- pattern-inside: |
$X = "..." % ...
...
- pattern-inside: |
$X = "..." + ...
...
- pattern-inside: |
$X = ... + "..."
...
- pattern-inside: |
$X = f"...{...}..."
...
- pattern-not-inside: |
$X = "..."
...
Examples
directly-returned-format-string.py
# -*- coding: utf-8 -*-
import os
import sqlite3
from flask import Flask
from flask import redirect
from flask import request
from flask import session
from jinja2 import Template
app = Flask(__name__)
@app.route("/loginpage")
def render_login_page(thing):
# ruleid:directly-returned-format-string
return '''
<p>{}</p>
<form method="POST" style="margin: 60px auto; width: 140px;">
<p><input name="username" type="text" /></p>
<p><input name="password" type="password" /></p>
<p><input value="Login" type="submit" /></p>
</form>
'''.format(thing)
@app.route("/loginpage2")
def render_login_page2(thing):
# ruleid:directly-returned-format-string
return '''
<p>%s</p>
<form method="POST" style="margin: 60px auto; width: 140px;">
<p><input name="username" type="text" /></p>
<p><input name="password" type="password" /></p>
<p><input value="Login" type="submit" /></p>
</form>
''' % thing
@app.route("/loginpage3")
def render_login_page3(thing):
# ruleid:directly-returned-format-string
return '''
<p>%s</p>
<form method="POST" style="margin: 60px auto; width: 140px;">
<p><input name="username" type="text" /></p>
<p><input name="password" type="password" /></p>
<p><input value="Login" type="submit" /></p>
</form>
''' % (thing,)
@app.route("/loginpage4")
def render_login_page4():
thing = "blah"
# the string below is now detected as a literal string after constant
# propagation
# ok:directly-returned-format-string
return thing + '''
<form method="POST" style="margin: 60px auto; width: 140px;">
<p><input name="username" type="text" /></p>
<p><input name="password" type="password" /></p>
<p><input value="Login" type="submit" /></p>
</form>
'''
@app.route("/loginpage5")
def render_login_page5():
thing = "blah"
# same, now ok thx to the constant propagation
# ok:directly-returned-format-string
return f'''
{thing}
<form method="POST" style="margin: 60px auto; width: 140px;">
<p><input name="username" type="text" /></p>
<p><input name="password" type="password" /></p>
<p><input value="Login" type="submit" /></p>
</form>
'''
@app.route("/loginpage5")
def render_login_page5(thing):
# ruleid:directly-returned-format-string
return f'''
{thing}
<form method="POST" style="margin: 60px auto; width: 140px;">
<p><input name="username" type="text" /></p>
<p><input name="password" type="password" /></p>
<p><input value="Login" type="submit" /></p>
</form>
'''
# cf. https://raw.githubusercontent.com/Deteriorator/Python-Flask-Web-Development/53be4c48ffbe7d30a1bde5717658f6de81820360/demo/http/app.py
@app.route('/hello')
def hello():
name = request.args.get('name')
if name is None:
name = request.cookies.get('name', 'Human')
respones = '<h1>Hello, %s</h1>' % name
if 'logged_in' in session:
respones += '[Authenticated]'
else:
respones += '[Not Authenticated]'
# ruleid: directly-returned-format-string
return respones
@app.route('/hello2')
def hello2():
name = request.args.get('name')
if name is None:
name = request.cookies.get('name', 'Human')
respones = '<h1>Hello, {}</h1>'.format(name)
if 'logged_in' in session:
respones += '[Authenticated]'
else:
respones += '[Not Authenticated]'
# ruleid: directly-returned-format-string
return respones
@app.route('/totally_not_bad')
def totally_not_bad():
# ok
return (
"a" + "\n" +
"b"
)
if __name__ == '__main__':
app.run(debug=True)
Short Link: https://sg.run/Zv6o