Abstract
Finding a security problem while an application is still being developed is usually much easier than discovering it after deployment. Static Application Security Testing (SAST) helps with this by examining source code without having to run the program. In this article, I use Bandit, an open-source SAST tool, to analyze a small Python application with several intentionally vulnerable practices: a hardcoded password, a possible SQL injection, unsafe command execution, and weak password hashing. The first scan finds five issues, two of them classified as high severity. After correcting the code, the second scan reports no high- or medium-severity issues. This practical test shows that Bandit can be a useful first layer of security analysis, although its findings still need to be reviewed by a person.
Introduction
When testing an application, we often focus on a simple question: does it work? However, code can produce the expected result and still be insecure. It might expose a password, allow malicious input to modify an SQL query, or execute an operating-system command that an attacker can manipulate.
This is where Static Application Security Testing becomes useful. A SAST tool examines the source code and looks for patterns commonly associated with vulnerabilities. Because the application does not need to be running, developers can receive feedback while they are still writing or reviewing the code. This idea is often known as shift-left security: instead of leaving security checks until the end, they are introduced earlier in development.
For this exercise, I chose Bandit, a free and open-source tool included in OWASP’s list of source-code analysis tools. Bandit is designed specifically for Python. Internally, it converts each file into an Abstract Syntax Tree (AST) and applies a collection of security checks. The final report explains what it found, where it found it, and how severe and reliable the warning may be.
Why Bandit?
I selected Bandit mainly because it is straightforward to use. It does not require a complex server or a running application, so it works well for a short experiment like this one. At the same time, it can also be included in a larger development workflow.
- It is open source and easy to install through
pip. - It does not need the application to be running.
- It identifies common Python security problems.
- It classifies findings by severity and confidence.
- It supports text, JSON, HTML, CSV, XML, and SARIF reports.
- It can be used locally or integrated into CI/CD workflows.
These advantages make Bandit a practical starting point for improving Python code. Still, it is not a replacement for penetration testing, dependency scanning, dynamic testing, or manual review. Its role is to give developers quick and repeatable feedback about code that deserves closer attention.
Installation
The installation process was simple. With Python and pip already available, I first created a virtual environment to keep the tool separate from other Python projects:
python -m venv .venv
On Windows, activate it with:
.venv\Scripts\activate
On Linux or macOS:
source .venv/bin/activate
After activating the environment, I installed Bandit:
python -m pip install bandit
I then checked that the installation had worked:
bandit --version
On some systems, the bandit command may not be available directly in the operating system’s PATH. In that case, the following command works as an alternative:
python -m bandit --version
The Vulnerable Python Application
To test the tool, I created a small example that represents part of a user-management application. The vulnerabilities were added intentionally so I could observe how Bandit reported them:
import hashlib
import sqlite3
import subprocess
ADMIN_PASSWORD = "admin123"
def search_user(username):
connection = sqlite3.connect("users.db")
query = "SELECT * FROM users WHERE username = '%s'" % username
return connection.execute(query).fetchall()
def ping_host(host):
return subprocess.check_output("ping -n 1 " + host, shell=True)
def hash_password(password):
return hashlib.md5(password.encode()).hexdigest()
At first sight, the program is short and easy to understand. Nevertheless, nearly every function contains a risky practice. This makes it a useful example of how code can look functional while hiding important security problems.
Figure 1. Vulnerable Python application used for the first Bandit analysis.
Running the First Scan
I saved the code as vulnerable_app.py and ran the following command:
python -m bandit vulnerable_app.py
When analyzing an entire project instead of a single file, Bandit can scan directories recursively:
python -m bandit -r .
My test with Bandit 1.9.4 produced five findings:
| Test ID | Severity | Finding |
|---|---|---|
| B404 | Low | Use of the subprocess module requires security review |
| B105 | Low | Possible hardcoded password |
| B608 | Medium | Possible SQL injection through string-based query construction |
| B602 | High | A subprocess is executed with shell=True
|
| B324 | High | MD5 is used for a security-sensitive hash |
The report immediately made the most urgent problems visible: two findings were high severity, one was medium severity, and two were low severity. It also showed the affected line and provided a link to additional information for every issue.
Figure 2. Beginning of the first scan, including the B404 and B105 findings.
Figure 3. Medium- and high-severity findings detected in the vulnerable application.
Figure 4. First-scan summary: two low-, one medium-, and two high-severity issues.
Understanding the Findings
B105: Hardcoded password
The first warning concerns the administrator password stored directly in the source code. This may seem convenient during development, but anyone who can access the repository can also read the password. Even if it is removed later, it may remain in the Git history. A safer approach is to obtain secrets from protected environment variables or a dedicated secret manager.
B608: Possible SQL injection
The SQL statement is built by inserting username directly into a string. If this value comes from a user, an attacker could provide specially crafted input and alter the intended query. Parameterized queries solve this problem by keeping the SQL instruction separate from the supplied data.
B602: Command execution with shell=True
This was one of the two high-severity findings. The value of host is joined directly to a command that runs with shell=True. If the application accepts this value from an untrusted user, that person may be able to add another operating-system command. Passing the arguments as a list and disabling the shell greatly reduces this risk, although the input should still be validated.
B324: Weak MD5 hash
The second high-severity finding is the use of MD5 for password hashing. MD5 is fast, which is actually a disadvantage when protecting passwords because an attacker can attempt many guesses in a short time. It is also considered cryptographically broken for security-sensitive purposes. Passwords should instead use an algorithm such as Argon2, bcrypt, scrypt, or PBKDF2, together with a unique salt.
B404: Review of subprocess
Bandit also warns about the import of subprocess. Importing this module is not automatically a vulnerability. The warning is a reminder that starting external processes is a sensitive operation and that the way the module is used should be reviewed carefully.
Applying the Fixes
After reviewing the report, I corrected the application:
import hashlib
import os
import sqlite3
import subprocess
ADMIN_PASSWORD = os.environ["ADMIN_PASSWORD"]
def search_user(username):
connection = sqlite3.connect("users.db")
query = "SELECT * FROM users WHERE username = ?"
return connection.execute(query, (username,)).fetchall()
def ping_host(host):
return subprocess.check_output(
["ping", "-n", "1", host],
shell=False,
timeout=5,
)
def hash_password(password):
salt = os.urandom(16)
return hashlib.scrypt(
password.encode(),
salt=salt,
n=2**14,
r=8,
p=1,
).hex()
The main changes were:
- Reading the administrator password from an environment variable.
- Replacing string-built SQL with a parameterized query.
- Passing subprocess arguments as a list and using
shell=False. - Adding a timeout to the operating-system command.
- Replacing MD5 with the password-oriented
scryptfunction and a random salt.
For a real application, both the salt and the resulting hash would need to be stored so that the password could be verified later. A specialized password-hashing library would also be a good option because it can manage the encoded format and verification process safely.
Figure 5. Corrected implementation using an environment variable, a parameterized query, shell=False, and scrypt.
Running the Scan Again
Once the changes were complete, I scanned the corrected file:
python -m bandit secure_app.py
The difference was clear. The second scan reported:
- 0 high-severity issues
- 0 medium-severity issues
- 3 low-severity observations
Figure 6. Second-scan summary: no medium- or high-severity issues remained.
Three low-severity observations remained. They were related to importing subprocess, using a partially qualified executable path, and starting an external process even though the shell was disabled.
Initially, it might seem that the goal should be to make every warning disappear. However, this result helped me understand that a SAST report is not simply a score. Each warning needs context. In this example, the risk could be reduced further by validating host, using the executable’s absolute path, restricting the allowed destinations, or replacing the external command with a Python networking library.
Using Bandit in Development
In a development team, Bandit could be run whenever code is committed or a pull request is created. If the team only wants to display medium- and high-severity findings, it can use:
python -m bandit -r . -ll
Bandit can also generate a JSON report that other tools can process:
python -m bandit -r . -f json -o bandit-report.json
A simple team policy could be to run Bandit:
- Before committing security-sensitive changes.
- During pull-request validation.
- In the CI/CD pipeline.
- Before a release.
The official PyCQA Bandit Action can publish results through GitHub code scanning as well. Integrating the scan into CI/CD is useful because the security check no longer depends on someone remembering to run it manually. It can also prevent a previously corrected mistake from being introduced again.
Limitations
Although the results were useful, Bandit also has limitations:
- It focuses on Python source code.
- It mainly identifies known insecure patterns.
- It may produce false positives or warnings that need contextual analysis.
- It cannot prove that an application is completely secure.
- It does not replace tests that examine authentication, authorization, business logic, runtime behavior, infrastructure, or vulnerable third-party dependencies.
For these reasons, a successful Bandit scan should not be interpreted as proof that an application is completely secure. It is better understood as one part of a broader secure development process.
Conclusion
This exercise gave me a practical view of what a SAST tool can contribute during development. In only a few seconds, Bandit identified hardcoded credentials, unsafe SQL construction, dangerous shell execution, and weak password hashing. After I corrected the code, all high- and medium-severity findings disappeared.
What I found most useful was not simply the number of warnings, but the way the report connected each problem to a specific line and explained why it could be dangerous. Bandit is fast, accessible, and easy to add to a Python workflow. However, it does not make security decisions for the developer. Its findings must be understood and combined with secure design, manual review, dependency scanning, and runtime testing.
References
- OWASP Foundation. “Source Code Analysis Tools.” https://owasp.org/www-community/Source_Code_Analysis_Tools
- PyCQA. “Bandit Documentation.” https://bandit.readthedocs.io/
- PyCQA. “Bandit Source Repository.” https://github.com/PyCQA/bandit
- PyCQA. “Bandit Action.” https://github.com/PyCQA/bandit-action






Top comments (0)