-
-
Notifications
You must be signed in to change notification settings - Fork 626
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add markupsafe.Markup XSS plugin (#1225)
* Add markupsafe.Markup XSS plugin * Apply suggestions from code review Co-authored-by: Eric Brown <ericwb@users.noreply.github.com> --------- Co-authored-by: Eric Brown <ericwb@users.noreply.github.com>
- Loading branch information
Showing
8 changed files
with
216 additions
and
8 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,118 @@ | ||
# Copyright (c) 2025 David Salvisberg | ||
# | ||
# SPDX-License-Identifier: Apache-2.0 | ||
r""" | ||
============================================ | ||
B704: Potential XSS on markupsafe.Markup use | ||
============================================ | ||
``markupsafe.Markup`` does not perform any escaping, so passing dynamic | ||
content, like f-strings, variables or interpolated strings will potentially | ||
lead to XSS vulnerabilities, especially if that data was submitted by users. | ||
Instead you should interpolate the resulting ``markupsafe.Markup`` object, | ||
which will perform escaping, or use ``markupsafe.escape``. | ||
**Config Options:** | ||
This plugin allows you to specify additional callable that should be treated | ||
like ``markupsafe.Markup``. By default we recognize ``flask.Markup`` as | ||
an alias, but there are other subclasses or similar classes in the wild | ||
that you may wish to treat the same. | ||
Additionally there is a whitelist for callable names, whose result may | ||
be safely passed into ``markupsafe.Markup``. This is useful for escape | ||
functions like e.g. ``bleach.clean`` which don't themselves return | ||
``markupsafe.Markup``, so they need to be wrapped. Take care when using | ||
this setting, since incorrect use may introduce false negatives. | ||
These two options can be set in a shared configuration section | ||
`markupsafe_xss`. | ||
.. code-block:: yaml | ||
markupsafe_xss: | ||
# Recognize additional aliases | ||
extend_markup_names: | ||
- webhelpers.html.literal | ||
- my_package.Markup | ||
# Allow the output of these functions to pass into Markup | ||
allowed_calls: | ||
- bleach.clean | ||
- my_package.sanitize | ||
:Example: | ||
.. code-block:: none | ||
>> Issue: [B704:markupsafe_markup_xss] Potential XSS with | ||
``markupsafe.Markup`` detected. Do not use ``Markup`` | ||
on untrusted data. | ||
Severity: Medium Confidence: High | ||
CWE: CWE-79 (https://cwe.mitre.org/data/definitions/79.html) | ||
Location: ./examples/markupsafe_markup_xss.py:5:0 | ||
4 content = "<script>alert('Hello, world!')</script>" | ||
5 Markup(f"unsafe {content}") | ||
6 flask.Markup("unsafe {}".format(content)) | ||
.. seealso:: | ||
- https://pypi.org/project/MarkupSafe/ | ||
- https://markupsafe.palletsprojects.com/en/stable/escaping/#markupsafe.Markup | ||
- https://cwe.mitre.org/data/definitions/79.html | ||
.. versionadded:: 1.8.3 | ||
""" | ||
import ast | ||
|
||
import bandit | ||
from bandit.core import issue | ||
from bandit.core import test_properties as test | ||
from bandit.core.utils import get_call_name | ||
|
||
|
||
def gen_config(name): | ||
if name == "markupsafe_xss": | ||
return { | ||
"extend_markup_names": [], | ||
"allowed_calls": [], | ||
} | ||
|
||
|
||
@test.takes_config("markupsafe_xss") | ||
@test.checks("Call") | ||
@test.test_id("B704") | ||
def markupsafe_markup_xss(context, config): | ||
|
||
qualname = context.call_function_name_qual | ||
if qualname not in ("markupsafe.Markup", "flask.Markup"): | ||
if qualname not in config.get("extend_markup_names", []): | ||
# not a Markup call | ||
return None | ||
|
||
args = context.node.args | ||
if not args or isinstance(args[0], ast.Constant): | ||
# both no arguments and a constant are fine | ||
return None | ||
|
||
allowed_calls = config.get("allowed_calls", []) | ||
if ( | ||
allowed_calls | ||
and isinstance(args[0], ast.Call) | ||
and get_call_name(args[0], context.import_aliases) in allowed_calls | ||
): | ||
# the argument contains a whitelisted call | ||
return None | ||
|
||
return bandit.Issue( | ||
severity=bandit.MEDIUM, | ||
confidence=bandit.HIGH, | ||
cwe=issue.Cwe.XSS, | ||
text=f"Potential XSS with ``{qualname}`` detected. Do " | ||
f"not use ``{context.call_function_name}`` on untrusted data.", | ||
) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
--------------------------- | ||
B704: markupsafe_markup_xss | ||
--------------------------- | ||
|
||
.. automodule:: bandit.plugins.markupsafe_markup_xss |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,13 @@ | ||
import flask | ||
from markupsafe import Markup, escape | ||
|
||
content = "<script>alert('Hello, world!')</script>" | ||
Markup(f"unsafe {content}") # B704 | ||
flask.Markup("unsafe {}".format(content)) # B704 | ||
Markup("safe {}").format(content) | ||
flask.Markup(b"safe {}", encoding='utf-8').format(content) | ||
escape(content) | ||
Markup(content) # B704 | ||
flask.Markup("unsafe %s" % content) # B704 | ||
Markup(object="safe") | ||
Markup(object="unsafe {}".format(content)) # Not currently detected |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,9 @@ | ||
from bleach import clean | ||
from markupsafe import Markup | ||
|
||
content = "<script>alert('Hello, world!')</script>" | ||
Markup(clean(content)) | ||
|
||
# indirect assignments are currently not supported | ||
cleaned = clean(content) | ||
Markup(cleaned) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,6 @@ | ||
from markupsafe import Markup | ||
from webhelpers.html import literal | ||
|
||
content = "<script>alert('Hello, world!')</script>" | ||
Markup(f"unsafe {content}") | ||
literal(f"unsafe {content}") |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters