Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

New check: B113: TrojanSource - Bidirectional control characters #757

Open
wants to merge 10 commits into
base: main
Choose a base branch
from
2 changes: 1 addition & 1 deletion bandit/core/issue.py
Expand Up @@ -83,7 +83,7 @@ def __init__(
ident=None,
lineno=None,
test_id="",
col_offset=0,
col_offset=-1,
):
self.severity = severity
self.cwe = Cwe(cwe)
Expand Down
10 changes: 10 additions & 0 deletions bandit/core/node_visitor.py
Expand Up @@ -296,4 +296,14 @@ def process(self, data):
"""
f_ast = ast.parse(data)
self.generic_visit(f_ast)
# Run tests that do not require access to the AST,
# but only to the whole file source:
self.context = {
"file_data": self.fdata,
"filename": self.fname,
"lineno": 0,
"linerange": [0, 1],
"col_offset": 0,
}
self.update_scores(self.tester.run_tests(self.context, "File"))
return self.scores
6 changes: 5 additions & 1 deletion bandit/core/test_properties.py
Expand Up @@ -15,7 +15,11 @@ def checks(*args):
def wrapper(func):
if not hasattr(func, "_checks"):
func._checks = []
func._checks.extend(utils.check_ast_node(a) for a in args)
for arg in args:
if arg == "File":
func._checks.append("File")
else:
func._checks.append(utils.check_ast_node(arg))

LOG.debug("checks() decorator executed")
LOG.debug(" func._checks: %s", func._checks)
Expand Down
5 changes: 3 additions & 2 deletions bandit/core/tester.py
Expand Up @@ -43,7 +43,7 @@ def run_tests(self, raw_context, checktype):
tests = self.testset.get_tests(checktype)
for test in tests:
name = test.__name__
# execute test with the an instance of the context class
# execute test with an instance of the context class
temp_context = copy.copy(raw_context)
context = b_context.Context(temp_context)
try:
Expand All @@ -66,7 +66,8 @@ def run_tests(self, raw_context, checktype):
if result.lineno is None:
result.lineno = temp_context["lineno"]
result.linerange = temp_context["linerange"]
result.col_offset = temp_context["col_offset"]
if result.col_offset == -1:
result.col_offset = temp_context["col_offset"]
result.test = name
if result.test_id == "":
result.test_id = test._test_id
Expand Down
70 changes: 70 additions & 0 deletions bandit/plugins/trojansource.py
@@ -0,0 +1,70 @@
#
# SPDX-License-Identifier: Apache-2.0
r"""
=====================================================
B113: TrojanSource - Bidirectional control characters
ericwb marked this conversation as resolved.
Show resolved Hide resolved
=====================================================

This plugin checks for the presence of unicode bidirectional control characters
in Python source files. Those characters can be embedded in comments and strings
to reorder source code characters in a way that changes its logic.

:Example:

.. code-block:: none

>> Issue: [B113:trojansource] A Python source file contains bidirectional control characters ('\u202e').
ericwb marked this conversation as resolved.
Show resolved Hide resolved
Severity: High Confidence: Medium
ericwb marked this conversation as resolved.
Show resolved Hide resolved
Location: examples/trojansource.py:0:0
ericwb marked this conversation as resolved.
Show resolved Hide resolved

.. seealso::

.. [1] https://trojansource.codes/
.. [2] https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2021-42574

.. versionadded:: 1.7.2
ericwb marked this conversation as resolved.
Show resolved Hide resolved

""" # noqa: E501
from tokenize import detect_encoding

import bandit
from bandit.core import test_properties as test
ericwb marked this conversation as resolved.
Show resolved Hide resolved


BIDI_CHARACTERS = (
"\u202A",
"\u202B",
"\u202C",
"\u202D",
"\u202E",
"\u2066",
"\u2067",
"\u2068",
"\u2069",
"\u200F",
)


@test.test_id("B113")
ericwb marked this conversation as resolved.
Show resolved Hide resolved
@test.checks("File")
def trojansource(context):
with open(context.filename, "rb") as src_file:
encoding, _ = detect_encoding(src_file.readline)
with open(context.filename, encoding=encoding) as src_file:
for lineno, line in enumerate(src_file.readlines(), start=1):
for char in BIDI_CHARACTERS:
try:
col_offset = line.index(char) + 1
except ValueError:
continue
text = (
"A Python source file contains bidirectional"
" control characters (%r)." % char
)
return bandit.Issue(
severity=bandit.HIGH,
confidence=bandit.MEDIUM,
ericwb marked this conversation as resolved.
Show resolved Hide resolved
text=text,
lineno=lineno,
col_offset=col_offset,
)
5 changes: 5 additions & 0 deletions doc/source/plugins/trojansource.rst
@@ -0,0 +1,5 @@
------------------
B113: trojansource
ericwb marked this conversation as resolved.
Show resolved Hide resolved
------------------

.. automodule:: bandit.plugins.trojansource
5 changes: 5 additions & 0 deletions examples/trojansource.py
@@ -0,0 +1,5 @@
#!/usr/bin/env python3
# cf. https://trojansource.codes/ & https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2021-42574
access_level = "user"
if access_level != 'none‮⁦': # Check if admin ⁩⁦' and access_level != 'user
print("You are an admin.\n")
7 changes: 7 additions & 0 deletions examples/trojansource_latin1.py
@@ -0,0 +1,7 @@
#!/usr/bin/env python3
# -*- coding: latin-1 -*-
# cf. https://trojansource.codes & https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2021-42574
# Some special characters: ������
access_level = "user"
if access_level != 'none??': # Check if admin ??' and access_level != 'user
print("You are an admin.\n")
3 changes: 3 additions & 0 deletions setup.cfg
Expand Up @@ -133,6 +133,9 @@ bandit.plugins =
snmp_insecure_version = bandit.plugins.snmp_security_check:snmp_insecure_version_check
snmp_weak_cryptography = bandit.plugins.snmp_security_check:snmp_crypto_check

# bandit/plugins/trojansource.py
trojansource = bandit.plugins.trojansource:trojansource

[build_sphinx]
all_files = 1
build-dir = doc/build
Expand Down
14 changes: 14 additions & 0 deletions tests/functional/test_functional.py
Expand Up @@ -888,3 +888,17 @@ def test_snmp_security_check(self):
"CONFIDENCE": {"UNDEFINED": 0, "LOW": 0, "MEDIUM": 0, "HIGH": 3},
}
self.check_example("snmp.py", expect)

def test_trojansource(self):
expect = {
"SEVERITY": {"UNDEFINED": 0, "LOW": 0, "MEDIUM": 0, "HIGH": 1},
"CONFIDENCE": {"UNDEFINED": 0, "LOW": 0, "MEDIUM": 1, "HIGH": 0},
}
self.check_example("trojansource.py", expect)

def test_trojansource_latin1(self):
expect = {
"SEVERITY": {"UNDEFINED": 0, "LOW": 0, "MEDIUM": 0, "HIGH": 0},
"CONFIDENCE": {"UNDEFINED": 0, "LOW": 0, "MEDIUM": 0, "HIGH": 0},
}
self.check_example("trojansource_latin1.py", expect)