Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
182 changes: 182 additions & 0 deletions bandit/plugins/logging_sensitive_info.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,182 @@
#
# Copyright 2026 RaccoonLabs
# SPDX-License-Identifier: Apache-2.0
"""
Bandit plugin to detect logging/printing of sensitive information.

Flags calls to logging.*, print(), pprint.*, and f-string output where
the arguments contain variable names associated with secrets:
password, secret, token, api_key, private_key, credential, auth_token, etc.
"""
import ast
import re

import bandit
from bandit.core import issue
from bandit.core import test_properties as test

# Sensitive variable name patterns
RE_SENSITIVE = re.compile(
r"(pas+wo?r?d|pass(phrase)?|pwd|secret|token|api_key|apikey|"
r"private_key|privatekey|access_key|accesskey|secret_key|secretkey|"
r"credential|auth_token|auth_token|bearer|api_secret|client_secret|"
r"database_url|db_password|db_pass|encryption_key|signing_key)",
re.IGNORECASE,
)

# Logging modules and functions to check
LOGGING_MODULES = {"logging", "logger"}
PRINT_FUNCTIONS = {
"print",
"pprint",
"pprint.pprint",
"debug",
"info",
"warning",
"warn",
"error",
"critical",
"exception",
"log",
}
LOGGING_METHODS = {
"debug",
"info",
"warning",
"warn",
"error",
"critical",
"exception",
"log",
"fatal",
}


def _is_sensitive_name(name: str) -> bool:
"""Check if a variable name looks like it holds sensitive data."""
return bool(RE_SENSITIVE.search(name))


def _check_node_for_sensitive(node) -> list:
"""Recursively check an AST node for references to sensitive variable names."""
found = []

if isinstance(node, ast.Name):
if _is_sensitive_name(node.id):
found.append(node.id)
elif isinstance(node, ast.Attribute):
if _is_sensitive_name(node.attr):
found.append(node.attr)
elif isinstance(node, ast.Subscript):
found.extend(_check_node_for_sensitive(node.value))
elif isinstance(node, ast.FormattedValue):
found.extend(_check_node_for_sensitive(node.value))
elif isinstance(node, ast.JoinedStr):
for value in node.values:
found.extend(_check_node_for_sensitive(value))
elif isinstance(node, ast.Call):
# Check keyring.get_password() and similar
if isinstance(node.func, ast.Attribute):
if node.func.attr in (
"get_password",
"get_credential",
"get_secret",
):
found.append(node.func.attr)
elif isinstance(node, ast.BinOp):
# f-string style: "Password: " + password
found.extend(_check_node_for_sensitive(node.left))
found.extend(_check_node_for_sensitive(node.right))
elif isinstance(node, (ast.Tuple, ast.List)):
for elt in node.elts:
found.extend(_check_node_for_sensitive(elt))

return found


def _is_logging_call(node: ast.Call) -> bool:
"""Check if a Call node is a logging or print call."""
func = node.func

# print(), pprint()
if isinstance(func, ast.Name) and func.id in PRINT_FUNCTIONS:
return True

# logging.debug(), logger.info(), etc.
if isinstance(func, ast.Attribute):
if func.attr in LOGGING_METHODS:
# Check if the object is a logger
if isinstance(func.value, ast.Name):
# Could be any logger instance or the logging module
return True
if isinstance(func.value, ast.Attribute):
return True

return False


@test.checks("Call")
@test.test_id("B622")
def logging_sensitive_info(context):
"""**B622: Test for logging of sensitive information**

This plugin detects when potentially sensitive information is passed
to logging or print calls. Sensitive variable names include:
password, secret, token, api_key, private_key, credential, etc.

**Config Options:**

None

:Example:

.. code-block:: none

>> Issue: [B622] Possible sensitive information logged: 'password'
Severity: Medium Confidence: Medium
CWE: CWE-532 (https://cwe.mitre.org/data/definitions/532.html)
Location: ./examples/sensitive_logging.py:5
4 def login(user, password):
5 logging.debug("Password: %s", password)

.. seealso::

- https://cwe.mitre.org/data/definitions/532.html
- https://owasp.org/www-community/vulnerabilities/Information_exposure_through_query_parameters_in_url

.. versionadded:: 1.9.0
"""
node = context.node

if not isinstance(node, ast.Call):
return None

if not _is_logging_call(node):
return None

# Check all arguments for sensitive variable references
sensitive_found = []
for arg in node.args:
sensitive_found.extend(_check_node_for_sensitive(arg))

for kw in node.keywords:
if kw.arg and _is_sensitive_name(kw.arg):
sensitive_found.append(kw.arg)
sensitive_found.extend(_check_node_for_sensitive(kw.value))

if not sensitive_found:
return None

# Deduplicate
unique_sensitive = list(dict.fromkeys(sensitive_found))

return bandit.Issue(
severity=bandit.MEDIUM,
confidence=bandit.MEDIUM,
cwe=issue.Cwe.CLEARTEXT_TRANSMISSION,
text=(
f"Possible sensitive information in logging/print call: "
f"'{', '.join(unique_sensitive)}'. "
f"Avoid logging passwords, tokens, API keys, or other secrets."
),
)
47 changes: 47 additions & 0 deletions examples/sensitive_logging.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
# Test cases for B622: logging of sensitive information

import logging

# B622: logging password
logging.debug("Password: %s", password)

# B622: logging secret
logging.info("Secret is %s", api_secret)

# B622: print token
print(f"Token: {auth_token}")

# B622: logging with keyword arg
logging.warning("Credentials: %s", credentials=credentials)

# B622: print api_key
print("The API key is", api_key)

# B622: logging private_key
logger.error("Key: %s", private_key)

# B622: logging access_key
logging.info("Access: %s" % access_key)

# No issue - safe logging
logging.debug("User logged in: %s", username)
print("Hello world")
logging.info("Request completed in %s seconds", elapsed)

# No issue - safe variable names
logging.debug("Count: %s", total_count)
print(f"Name: {user_name}")

# B622: pprint sensitive info
import pprint
pprint.pprint({"token": token})

# B622: logging database URL with password
logging.debug("DB URL: %s", database_url)

# B622: f-string with sensitive
print(f"Connecting with {db_password}")

# B622: keyring retrieval then logged
password = keyring.get_password("service", "user")
logging.debug("Got password: %s", password)
1 change: 1 addition & 0 deletions setup.cfg
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,7 @@ bandit.plugins =
hardcoded_password_string = bandit.plugins.general_hardcoded_password:hardcoded_password_string
hardcoded_password_funcarg = bandit.plugins.general_hardcoded_password:hardcoded_password_funcarg
hardcoded_password_default = bandit.plugins.general_hardcoded_password:hardcoded_password_default
logging_sensitive_info = bandit.plugins.logging_sensitive_info:logging_sensitive_info

# bandit/plugins/general_hardcoded_tmp.py
hardcoded_tmp_directory = bandit.plugins.general_hardcoded_tmp:hardcoded_tmp_directory
Expand Down