Small Python scripts are often treated as low-risk tools because they are short, local, and built to solve a very specific task. However, a script that manipulates files, interprets input, or launches operating-system commands can still introduce serious security problems if it is written carelessly. Bandit is a static analysis tool for Python that helps detect insecure coding patterns before the script is executed. In this article, I use Bandit to review a simple file automation script that generates directory summaries.
The script looks useful at first glance, but it includes unsafe command construction, shell=True, and eval(). After identifying those risks, I rewrite the script using safer Python patterns. This example shows that even small automation tools deserve a security review and that SAST can provide useful feedback early in development.
Introduction
Many Python scripts are written quickly to automate repetitive work: list files, rename documents, generate reports, clean directories, or move content from one place to another. Because these tools are usually short and practical, it is easy to assume they are harmless.
That assumption can be misleading. A script does not need to be part of a large web platform to become risky. If it reads user input, builds shell commands, or writes files without enough care, it can expose the system to avoidable problems.
This is why Static Application Security Testing, or SAST, is still relevant for small programs. OWASP includes Bandit among its source code analysis tools, and Bandit is specifically designed to detect common security issues in Python code without executing the program.
In this article, I apply Bandit to a small file automation script. The goal is not to create a dramatic vulnerability demo, but to show how common coding habits in local utilities can still produce unsafe behavior.
Why this topic matters
When security is discussed in software projects, the attention usually goes to APIs, authentication, databases, or cloud infrastructure. Those areas are important, but smaller tools also deserve attention. In many teams, simple scripts become part of the daily workflow and are reused over time.
For example, a script that summarizes a directory for reporting purposes may later be integrated into a scheduled task, shared with another teammate, or adapted into a larger internal tool. If the original script contains unsafe patterns, those patterns can spread just as easily as bugs in any other part of the system.
That is one reason static analysis matters. It helps developers notice risky decisions before those decisions are normalized in future versions of the code.
Why Bandit is a practical choice
Bandit is a practical choice for Python because it focuses on source code scanning and looks for patterns that are often associated with vulnerabilities. According to the Bandit documentation, the tool parses Python files, builds an abstract syntax tree, applies security checks, and generates a report with the issues it detects.
This makes it useful in two ways. First, it gives fast feedback during development. Second, it can be integrated into automated workflows such as GitHub-based scanning through the official Bandit Action, which helps security checks happen consistently instead of relying only on memory.
The example script
For this experiment, I created a simple Python script that receives a folder path and generates a basic summary of its content. This is the type of utility many students or developers might actually write during a project.
At first glance, the script seems functional. It lists files, saves a report, and includes a helper function to process a user-provided expression. But several implementation choices make it unsafe.
import os
import subprocess
def summarize(folder):
command = "ls " + folder
output = subprocess.check_output(command, shell=True)
return output.decode()
def save_report(name, content):
file = open(name, "w")
file.write(content)
file.close()
def run_expression(value):
return eval(value)
This script contains at least three important concerns:
- It builds an operating-system command by concatenating user input.
- It executes that command with
shell=True. - It evaluates arbitrary input with
eval().
None of these choices prevent the script from working. That is exactly what makes them useful for a SAST exercise: the code appears valid, but the implementation includes risky behavior.
Why the code is risky
The summarize() function is the most obvious problem. It constructs a shell command by joining ls with the value of folder, then passes that string to subprocess.check_output(..., shell=True). Bandit documents this pattern as dangerous because shell-based subprocess execution can be vulnerable to injection attacks, especially when the command string is built dynamically.
The use of eval() is also problematic. Even though it may look convenient for converting or calculating values, it can execute arbitrary Python expressions if it receives untrusted input. In a small utility script, that kind of flexibility is usually unnecessary.
The file-writing logic is less dramatic, but still worth improving. Using a context manager is safer and more maintainable because it ensures proper file handling and makes the code clearer.
Running Bandit
After saving the file as automation_tool.py, the scan command is straightforward:
python -m bandit automation_tool.py
If the code were part of a larger project, Bandit could also scan directories recursively:
python -m bandit -r .
Bandit typically reports subprocess-related issues when external commands are used in unsafe ways, and its documentation includes a specific rule for shell=True in subprocess calls.
A simplified example of the kind of result Bandit may produce is the following:
>> Issue: [B602] subprocess call with shell=True identified
Severity: High Confidence: High
>> Issue: [B404] Consider possible security implications associated with subprocess
Severity: Low Confidence: High
The exact output may vary depending on the Bandit version, but the important point is that the report helps connect each warning to a concrete coding pattern.
What the findings mean
One useful aspect of Bandit is that it does not just say that the script is “bad.” It points to specific practices that deserve review.
In this example, the most important finding is the shell-based subprocess execution. Bandit explains that calls using shell=True are risky because they may allow shell injection if input is not carefully controlled.
A practical example makes this easier to understand. Imagine that a teammate runs the script with a folder name that contains shell metacharacters or unexpected concatenated content. Even if the input is not intentionally malicious, command construction through string concatenation creates unnecessary exposure.
In other words, the script is not dangerous because it lists files; it becomes dangerous because of how it chooses to do that.
A safer rewrite
The same goal can be achieved with safer Python features and without invoking the system shell.
from pathlib import Path
def summarize(folder):
target = Path(folder)
if not target.is_dir():
raise ValueError("Invalid folder")
files = [p.name for p in target.iterdir() if p.is_file()]
return "\n".join(files)
def save_report(name, content):
path = Path(name)
with path.open("w", encoding="utf-8") as file:
file.write(content)
def parse_number(value):
return int(value)
This new version changes the design in several important ways:
- It uses
Pathobjects instead of building shell commands. - It validates that the provided path is actually a directory.
- It writes files using a context manager.
- It replaces
eval()with a specific conversion function.
The script still solves the same task, but it does so with a more controlled and predictable approach.
A practical comparison
The contrast between both versions is useful because it shows how insecure code is not always complex code. In the first version, the script depends on system command execution for something Python can do directly. In the second version, the logic stays inside the language and becomes easier to reason about.
That difference matters in real development work. The more a script relies on raw command strings or flexible execution features, the harder it becomes to guarantee safe behavior.
How this applies in real projects
This example may look small, but the pattern is common. A developer creates a quick utility, another person reuses it, and then the script becomes part of a shared workflow.
For instance, imagine an internal tool that generates nightly summaries of uploaded files. If the original implementation trusts input too much or depends on shell execution, the risk can remain hidden for a long time because the script “works.” Bandit helps uncover those kinds of problems before the code becomes permanent.
That is also why CI/CD integration can be valuable. The official PyCQA Bandit Action allows teams to run scans automatically in GitHub-based workflows, reducing the chance that insecure patterns are introduced silently during future changes.
A minimal workflow example is:
name: Bandit Scan
on: [push, pull_request]
jobs:
analyze:
runs-on: ubuntu-latest
permissions:
security-events: write
actions: read
contents: read
steps:
- uses: actions/checkout@v4
- name: Perform Bandit Analysis
uses: PyCQA/bandit-action@v1
Limitations
Although Bandit is useful, it is not enough by itself. It focuses on Python source code and mainly detects patterns that are already known to be risky. That means it cannot fully understand business logic problems, misuse of permissions, or every contextual issue in a script.
It may also produce warnings that require human judgment. For example, some subprocess usage may be intentional and controlled, but still deserves review because process execution is a sensitive action.
For that reason, Bandit works best as one layer in a broader secure development process that also includes code review, input validation, safer defaults, and runtime testing.
Final thoughts
This exercise reinforced an important idea for me: a script does not need to be large to deserve security analysis. In fact, small automation tools are often trusted too quickly because they look simple.
Bandit is helpful precisely because it slows that assumption down. By scanning the source code before execution, it helps reveal insecure choices that are easy to overlook during normal development.
The most useful lesson was not only that Bandit can flag a dangerous pattern like shell=True, but that it encourages a better question while writing Python utilities: not just “does this work?”, but also “is this the safest way to solve the task?”
Top comments (0)