Your own code¶
Attacks on the supply chain do not only stem from :doc:`dependencies; your own code can also provide points of entry. A PyPI token hard-coded into the source code, once uploaded to a public repository, provides everything needed to launch an attack, compromise your account and publish malicious packages under your name. Apart from secrets, common security flaws are often hidden in everyday coding patterns that may initially appear harmless during a code review and can be overlooked by humans. Detecting these using a linter is the first line of defence.
The eternal secret¶
Leaked credentials are the starting point for many security breaches in the supply chain. An exposed PyPI token makes it possible to publish versions of your packages containing backdoors. An exposed database URL makes it possible to steal data. And yet, this pattern is all too common. It is better to use environment variables:
import os
DATABASE_KEY = os.environ["DB_KEY"]
DATABASE_URL = os.environ["DB_URL"]
Warning
Git never forgets: once you’ve managed a secret via Git, it remains in your repository’s history forever. Simply deleting it in a later commit doesn’t really help. Anyone with access to the repository can extract these credentials. In attacks, the Git history is often scoured for secrets first, and a PyPI token or cloud credentials that have been exposed are often the first step in a supply chain compromise.
Cryptographic vulnerabilities¶
Other common security vulnerabilities include cryptographic weaknesses such as MD5 and SHA-1. MD5 collisions were first demonstrated in 2004 and SHA-1 collisions in 2017. This means that collisions can be generated using different inputs that produce the same hash value. This enables the forgery of certificates, the manipulation of downloads or the circumvention of integrity checks. Therefore, do not use either of these algorithms for security purposes; instead, use SHA-256 or better:
import hashlib
digest = hashlib.sha256(payload).hexdigest()
Stalled connections¶
This may be subtle, but it is nonetheless dangerous, as a slow server can bring your process to a standstill indefinitely. An attack via such a server, with which your application communicates, can bring every request to a standstill, exhaust your thread pool and trigger a denial-of-service attack. Your entire application will then grind to a halt because you forgot to specify a parameter. You should therefore always specify a timeout:
>>> import httpx
>>> r = httpx.get("https://httpbin.org/get", timeout=30)
httpx.ReadTimeout: The read operation timed out
Detect security vulnerabilities with Ruff¶
Ruff is a fast Python linter that includes comprehensive security rules from Bandit:
$ uvx ruff check --select S .
See also
Further information can be found in the Ruff security rules documentation.
For future checks, you can configure ruff in the pyproject.toml
file:
[tool.ruff]
lint.select = ["S"]
The ["S"] security rules, which use Bandit checks, detect hard-coded
secrets, weak encryption and insecure deserialisation. Ruff runs in less than a
second, so you can run it whilst typing in your IDE and before every commit. All
three vulnerabilities mentioned above are detected, along with many more,
including:
Rule |
Description |
Hard-coded secrets |
|
Pickle and other insecure deserialisation |
|
Use of |
|
Missing timeouts |
|
Weak cryptography, such as MD5 collisions |
|
SQL injection via string formatting |
See also
You can also integrate Bandit into Jupyter Notebooks, IDEs and prek.
You can also use Pysa for taint analysis.
For GitHub repositories, you can alternatively use CodeQL; see also codeql-action.
Trusted publishing¶
In an earlier section, we’ve already provided some guidance on how to secure the publication of Python packages on PyPI: