Python Security Best Practices – Part 5
Welcome back to our comprehensive series on Python security. In previous installments, we laid the groundwork by covering fundamental security principles. Now, in Part 5, we elevate the discussion to address the nuanced, advanced threats that developers face in today’s complex application landscape. While basics like preventing SQL injection and Cross-Site Scripting (XSS) are crucial, modern Python applications, often deployed as microservices in containerized environments, present a new set of challenges. This article dives deep into these advanced topics, moving beyond the textbook examples to tackle real-world vulnerabilities.
We will explore critical areas often overlooked in standard security checklists, including the treacherous waters of insecure deserialization, the pervasive risks within your software supply chain, and the subtle but significant dangers of misconfigured runtime environments. Our focus will be on practical, actionable implementations that you can apply directly to your projects. Staying current with the latest security advisories and python news is vital, as new vulnerabilities are discovered constantly. This guide will equip you with the knowledge to harden your applications against sophisticated attacks, ensuring your code is not only functional but also resilient and secure by design.
Advanced Vulnerability Prevention: Beyond the OWASP Top 10
While the OWASP Top 10 provides an essential foundation for web application security, a mature security posture requires looking beyond the most common vulnerabilities. Advanced threats often exploit the intricate interactions between your code, its dependencies, and the environment it runs in. Here, we’ll dissect two potent threats: Insecure Deserialization and Server-Side Request Forgery (SSRF).
Taming the Beast: Insecure Deserialization
Serialization is the process of converting a Python object into a byte stream to store it or transmit it across a network. Deserialization is the reverse process. While incredibly useful, it becomes a massive security hole when deserializing data from an untrusted source, as it can lead to Remote Code Execution (RCE).
The most notorious culprit in the Python ecosystem is the pickle module. It is powerful because it can serialize almost any Python object, including code. If an attacker can control the pickled data being deserialized, they can execute arbitrary commands on your server.
Consider this dangerous code snippet:
import pickle
import base64
import os
# Attacker creates a malicious payload
class RCE:
def __reduce__(self):
cmd = ('rm -rf /tmp/important-file')
return os.system, (cmd,)
malicious_payload = base64.b64encode(pickle.dumps(RCE()))
# Server receives and deserializes the payload (DO NOT RUN THIS)
# user_data = base64.b64decode(malicious_payload)
# pickle.loads(user_data)
In this example, the __reduce__ magic method is hijacked to execute a system command when the object is unpickled. The best practice is simple: Never, ever unpickle data from an untrusted or unauthenticated source.
Best Practices:
- Use Safer Formats: For data interchange, prefer formats like JSON, which are not executable. They are limited to simple data types, preventing code execution vulnerabilities.
- Vet Your Libraries: Be cautious with other libraries that perform deserialization. For example, when using PyYAML, always use
yaml.safe_load()instead of the dangerousyaml.load(). The latter can, much likepickle, construct arbitrary Python objects.
Mitigating Server-Side Request Forgery (SSRF)
SSRF is a vulnerability where an attacker can coerce a server-side application to make HTTP requests to an arbitrary domain of the attacker’s choosing. This can be used to pivot into internal networks, scan for open ports, access metadata services in cloud environments (like the AWS EC2 metadata service at 169.254.169.254), or interact with other internal services that are not exposed to the public internet.

A vulnerable Flask application might look like this:
from flask import Flask, request
import requests
app = Flask(__name__)
@app.route('/fetch-image')
def fetch_image():
image_url = request.args.get('url')
try:
# The server makes a request to a user-supplied URL
response = requests.get(image_url)
return response.content, 200, {'Content-Type': response.headers['Content-Type']}
except requests.exceptions.RequestException as e:
return str(e), 500
if __name__ == '__main__':
app.run(debug=True)
An attacker could provide a URL like http://169.254.169.254/latest/meta-data/iam/security-credentials/ to steal cloud credentials or http://localhost:8080/admin to access an internal admin panel.
Best Practices:
- Use an Allow-List: The most effective defense is to maintain a strict allow-list of domains, IPs, and ports that the application is permitted to request. Reject any request that does not match.
- Validate and Sanitize: If an allow-list is not feasible, rigorously validate the URL. Ensure it points to the expected protocol (e.g., only HTTP/HTTPS) and doesn’t resolve to an internal or private IP address range.
- Use Dedicated Libraries: Libraries like
ssrf-filtercan help by providing utilities to validate URLs against common SSRF targets.
Securing Your Supply Chain: Dependencies and Tooling
Your application is not just the code you write; it’s also the vast ecosystem of open-source libraries you depend on. This software supply chain is a significant attack vector. A single vulnerable dependency, even a transitive one (a dependency of your dependency), can compromise your entire application.
The Hidden Dangers in requirements.txt
Managing Python dependencies is critical for security. Failing to do so exposes you to several risks:
- Vulnerable Packages: A legitimate package might have a known vulnerability (CVE) in the version you are using.
- Typosquatting: Attackers publish malicious packages with names similar to popular ones (e.g.,
python-nmapvs. the maliciouspython3-nmap), hoping a developer makes a typo during installation. - Dependency Confusion: An attacker can publish a package with the same name as an internal, private package to a public repository. If the build system is not configured correctly, it might pull the malicious public version instead.
The key to mitigating these risks is disciplined dependency management. Simply listing top-level dependencies like requests is not enough. You must pin the exact versions of all packages, including transitive ones.
Best Practices:
- Pin Everything: Use tools like
pip-toolsto compile a high-levelrequirements.infile into a fully-pinnedrequirements.txt. This ensures reproducible and predictable builds.# requirements.in django==4.1 requests # Run: pip-compile requirements.in # Produces requirements.txt with all dependencies pinned: # django==4.1 # requests==2.28.1 # charset-normalizer==2.1.1 # via requests # ... and so on - Use Hashes: For an even higher level of security, use hash-checking with
pip. You can generate a file with hashes for each package (pip-compile --generate-hashes). This ensures the package you download hasn’t been tampered with since you pinned it.
Automated Dependency Scanning in CI/CD
Manually checking every dependency for vulnerabilities is impossible. Security scanning must be an automated part of your development lifecycle, integrated directly into your Continuous Integration/Continuous Deployment (CI/CD) pipeline.

Tools like pip-audit (from the Python Packaging Authority), safety, and platforms like GitHub’s Dependabot, Snyk, and GitLab Security Scanning work by checking your pinned dependencies against a database of known vulnerabilities.
Here’s how you can use pip-audit:
# Install pip-audit
python -m pip install pip-audit
# Run it against your requirements file
pip-audit -r requirements.txt
If a vulnerability is found, the tool will report it, often including details about the affected versions and a link to the CVE. This allows you to act immediately.
Best Practices:
- Fail the Build: Configure your CI pipeline to fail if the scanner finds any high or critical severity vulnerabilities. This forces developers to address the issue before the code is merged.
- Regular Scans: Schedule regular scans of your main branch, even if no code has changed. New vulnerabilities are discovered in old packages all the time.
- Automate Updates: Use tools like Dependabot to automatically create pull requests to update vulnerable dependencies, making it easy for your team to stay on top of security patches.
Hardening the Python Runtime and Environment
A secure application can still be compromised if it runs in an insecure environment. Hardening the runtime involves applying the Principle of Least Privilege and managing secrets properly, ensuring your application has only the permissions and access it strictly needs.
Applying the Principle of Least Privilege
Your Python application should not run with more permissions than necessary. Running as the root user is a common but dangerous mistake. If an attacker finds an RCE vulnerability in a root-level process, they gain full control of the host machine.
Best Practices:
- Use a Non-Root User: In your Dockerfile or deployment scripts, always create a dedicated, unprivileged user to run your application.
# In your Dockerfile FROM python:3.10-slim WORKDIR /app # Create a non-root user RUN addgroup --system app && adduser --system --group app COPY --chown=app:app . . # Switch to the non-root user USER app CMD ["python", "app.py"] - Filesystem Permissions: Ensure your application user only has write access to the directories it needs (e.g., a temporary upload folder). Configuration files and source code should be read-only. Secrets files should not be world-readable.
- Network Isolation: Use container orchestration tools (like Kubernetes) and cloud security groups to restrict network traffic. If your web application only needs to talk to a database on port 5432, block all other outbound traffic.
Securely Managing Secrets and Configuration
Hardcoding secrets like API keys, database passwords, and encryption keys in your source code is one of the most common and damaging security anti-patterns. These secrets will end up in your version control system, accessible to anyone who can read the code.

Best Practices:
- Use Environment Variables: The simplest approach is to store secrets in environment variables. Python can access them via
os.environ. This separates configuration from code.import os # Load the database password from an environment variable db_password = os.environ.get("DB_PASSWORD") if not db_password: raise ValueError("DB_PASSWORD environment variable not set!") - Use a Dedicated Secrets Manager: For a more robust and scalable solution, use a secrets management tool like HashiCorp Vault, AWS Secrets Manager, or Google Cloud Secret Manager. These services provide centralized management, auditing, and automatic rotation of secrets. Your application authenticates with the service at startup and fetches the secrets it needs dynamically.
- Encrypt Configuration Files: If you must use configuration files, encrypt them at rest. Tools like Ansible Vault or libraries like
cryptographycan be used to manage encrypted YAML or INI files.
Practical Implementation and Modern Authentication
Let’s bring these concepts together with a practical checklist and a look at modern authentication patterns that are essential for securing today’s APIs and web applications.
Moving Beyond Simple Passwords with JWTs
For APIs and microservices, session-based authentication can be cumbersome. JSON Web Tokens (JWTs) provide a standard for creating self-contained, stateless access tokens. A JWT contains a header, a payload (with claims like user ID and expiration time), and a signature. The server signs the token with a secret key, and on subsequent requests, it verifies the signature to ensure the token is authentic and hasn’t been tampered with.
Best Practices for JWTs:
- Use Strong Algorithms: Avoid the
HS256algorithm if the secret key could be compromised. Use an asymmetric algorithm likeRS256, where the server signs with a private key and verifies with a public key. This prevents an attacker who steals the public key from being able to forge new tokens. - Validate Claims: Always validate the standard claims, especially the expiration time (
exp) to prevent replay attacks. - Keep Payloads Lean: Don’t store sensitive information in the JWT payload, as it is only Base64 encoded, not encrypted.
A Practical Python Security Checklist
Here is a summary of actionable steps you can take to implement the advanced practices discussed in this article:
- Audit for Deserialization: Search your codebase for uses of
pickle.loads(),dill.loads(), andyaml.load(). Replace them with safer alternatives likejson.loads()oryaml.safe_load()wherever possible. - Implement Dependency Scanning: Add a tool like
pip-auditto your CI/CD pipeline and configure it to fail the build on high-severity vulnerabilities. - Pin and Hash Dependencies: Use
pip-toolsto generate a fully pinned and hashedrequirements.txtfile to ensure build reproducibility and integrity. - Adopt a Secrets Management Tool: Move all secrets out of your codebase and into environment variables or a dedicated service like AWS Secrets Manager or HashiCorp Vault.
- Containerize as a Non-Root User: Update your Dockerfiles to create and switch to an unprivileged user before running the application.
- Validate Outbound Requests: If your application fetches resources from user-provided URLs, implement a strict allow-list or robust validation to prevent SSRF attacks.
- Use Modern Authentication: For APIs, implement a token-based authentication system using JWTs with a strong signing algorithm (e.g., RS256).
Conclusion
Securing a Python application in 2023 and beyond is a multi-faceted challenge that extends far beyond basic vulnerability patching. As we’ve explored in this fifth installment, true resilience comes from a deep understanding of advanced threats like insecure deserialization and SSRF, a disciplined approach to software supply chain security, and a commitment to hardening the environment where your code executes. Security is not a feature to be added at the end of a development cycle; it is a continuous process of vigilance, automation, and education.
By implementing automated dependency scanning, adopting the Principle of Least Privilege, managing secrets responsibly, and using modern authentication patterns, you can build a robust defense-in-depth strategy. We encourage you to stay informed by following reputable security sources and the latest “python news”, as the threat landscape is always evolving. Building a culture of security within your team is the ultimate best practice, ensuring that every line of code is written with a security-first mindset.
