Join our Newsletter — 33% off our NHI Course
Home› Guides› Python Security Guide: Dependencies, Unsafe Patterns and Secrets
Guide Application Security

Python Security Guide: Dependencies, Unsafe Patterns and Secrets

PDF for newsletter subscribers · free to read online

← All guides
By Lalit Choda, NHI Mgmt Group Updated 24 October 2026 17 min read
On this page

Most serious Python security incidents now start in the packaging and build chain, not in the application's own logic: a poisoned package, a leaked PyPI token or a startup hook that runs on every interpreter launch. The language itself adds a second layer of risk through features that execute code by design, such as pickle, yaml.load, eval and shell=True. This guide covers how to install dependencies reproducibly and verifiably, how to publish packages without long-lived tokens, which coding patterns to ban and what to use instead, and how to keep secrets out of source, bytecode and images. It is written for Python developers, platform and DevSecOps engineers, and security teams who review Python services, data pipelines and AI tooling.

Key takeaways

  • Install from hashes, not from names. pip's hash-checking mode (--require-hashes) with a fully pinned lockfile stops a swapped or tampered artefact from installing, and --only-binary :all: stops source builds from running arbitrary build code.
  • Publish with trusted publishing. PyPI exchanges a CI identity token for an API token that expires after 15 minutes, so there is no long-lived upload token to steal; delete old tokens once it works.
  • Treat deserialisation as code execution. pickle.load and yaml.load on untrusted data can run arbitrary code; use JSON, yaml.safe_load or signed, schema-validated formats.
  • Remember the startup hooks. import lines in .pth files run every time Python starts, which is why malicious packages use them; monitor site-packages for new .pth files.
  • Scan what you ship. Secrets survive in __pycache__ bytecode, wheels and container layers even when they were never committed, so scanning the repository alone is not enough.

The Python threat model in one table

Python risks group into four areas. The table maps each to the main attacker technique and the control that addresses it. ATT&CK IDs are from ATT&CK v20.

AreaTypical failureATT&CK techniquePrimary control
DependenciesMalicious or hijacked package on PyPI, typosquat, dependency confusionT1195.001 Compromise Software Dependencies and Development ToolsPrivate index or proxy, pinned lockfile with hashes, vulnerability and malware scanning
PublishingStolen PyPI API token used to upload a release outside the normal pipelineT1195.002 Compromise Software Supply ChainTrusted publishing, protected release environment, attestations
Runtime code pathsUntrusted input reaches pickle, yaml.load, eval, exec or a shellT1059.006 Command and Scripting Interpreter: PythonBanned-API list enforced by Bandit, safe alternatives, input validation
Persistence in the interpreterMalicious .pth or sitecustomize code runs on every startT1546.018 Python Startup HooksRead-only site-packages, file integrity monitoring, clean images
SecretsTokens in code, .env, bytecode or imagesT1552.001 Credentials In FilesSecret scanning of repos and artefacts, short-lived credentials

For the cross-ecosystem picture see the Software Supply Chain Security Guide and the Malicious Package and Dependency Confusion Guide.

Dependencies: lockfiles, hashes and install-time code

By default pip does no checking against remote tampering and will run arbitrary code from a distribution while installing it, as the pip documentation states. Source distributions (sdists) run build backends at install time; wheels do not. Hardening therefore has three parts: pin everything, verify every artefact, and prefer wheels.

Pin and hash

  • Hash-checking mode: pin each requirement with == and list one or more --hash=sha256:<digest> values. Adding a hash to any line switches the whole file into hash-checking mode, and every dependency, including transitive ones, must then be pinned and hashed or pip fails. Pass --require-hashes on the command line to force the mode even when a file has no hashes, which catches a file that was generated without them.
  • Algorithms: pip recommends sha256 and rejects md5, sha1 and sha224 for this purpose. The #sha256= fragment PyPI puts in download URLs only protects against corruption and does not satisfy --require-hashes.
  • Generating the lock: pip-compile --generate-hashes requirements.in (pip-tools) writes a hashed requirements.txt; pip-sync then makes the virtual environment match it exactly. Poetry, PDM and uv produce their own lockfiles (poetry.lock, pdm.lock, uv.lock), and pylock.toml is the standard lockfile format; all of these are read by OSV-Scanner.
  • Wheels only: --only-binary :all: refuses sdists, so no package build code runs on the build host. Where a dependency has no wheel, build it once in an isolated job and publish the wheel to your internal index.
  • Do not bypass pip: the pip documentation advises against python setup.py install, setup.py develop and easy_install, which can fetch unchecked dependencies. Install your own project with python -m pip install --no-deps . after the hashed dependencies are in place.

A minimal hashed install in CI looks like this:

python -m pip install --require-hashes --only-binary :all: --no-deps -r requirements.txt
python -m pip install --no-deps .

Where packages come from

  • One index: point pip at a single internal proxy or mirror rather than mixing --index-url and --extra-index-url. With an extra index, pip chooses the best version across all indexes, which is the condition dependency confusion relies on. The PyTorch torchtriton attack worked this way: a public PyPI package registered with the same name as a dependency served from the project's own nightly index took precedence, and nightly installs pulled it in.
  • Reserve your names: register your internal package names on PyPI or use a namespace prefix that only your index serves.
  • Quarantine new versions: many malicious releases are removed within hours. A proxy that delays brand-new versions for a few days, combined with scheduled lockfile updates rather than floating ranges, removes most of that exposure.

Broader open-source intake rules are in the Open Source Security Guide.

Publishing safely: trusted publishing and attestations

Package maintainers and any organisation that publishes internal or public Python packages hold a credential that can push code into other people's environments. That credential is the target.

Trusted publishing

PyPI trusted publishing uses OpenID Connect. The maintainer registers a specific CI configuration (for example a GitHub repository, workflow file and environment) as a trusted publisher for the project. At release time the CI job presents its OIDC identity token, PyPI checks it against the registered configuration, and if it matches, PyPI returns an API token scoped to that project and valid for 15 minutes. PyPI describes this as complementing API tokens, so an old token keeps working until someone deletes it.

  • Bind to an environment: on GitHub, register the publisher with a named deployment environment and require reviewers on that environment. PyPI's own analysis of the Ultralytics attack recommends GitHub Environments with trusted publishers.
  • Remove the fallback: once trusted publishing has produced a release, revoke every project and account API token. PyPI notes that a token that goes unused while trusted publishing works is a strong signal it can be revoked.
  • Isolate the release job: the job that receives id-token: write should not restore caches written by pull-request jobs or run third-party scanners. See the GitHub and GitHub Actions Security Guide.

Attestations (PEP 740)

PyPI accepts digital attestations on uploaded files, built on the in-toto attestation framework. Two predicates are supported: SLSA Provenance, which records where the package was built from, and PyPI Publish, which records that the file was published through a particular trusted publisher. Each file can carry at most one of each. PyPI currently accepts attestation identities from GitHub Actions, GitLab CI/CD, Google Cloud and ActiveState. Consumers and incident responders can use them to tell a release built by the real workflow from one uploaded with a stolen token, which is exactly how PyPI separated the two waves of the Ultralytics compromise. Attestations prove origin, not safety: a compromised workflow still produces a valid attestation.

Publishing methodCredential lifetimeStolen credential lets an attackerUse when
Account-wide API tokenUntil revokedUpload to every project the account ownsAvoid
Project-scoped API tokenUntil revokedUpload to one project from anywhereCI provider not supported by trusted publishing; store in a secrets manager and rotate
Trusted publishing15 minutesLittle after expiry; attacker needs to run inside the registered workflowDefault for GitHub Actions, GitLab CI/CD and other supported providers
Trusted publishing plus protected environment and attestations15 minutesNeeds workflow compromise and reviewer approvalWidely used or security-sensitive packages

Publishing tokens are machine credentials and belong in the same inventory as other non-human identities. See the Secrets Management Guide.

Unsafe patterns and their safe alternatives

Several standard features turn data into code. They are safe with data you created and control, and dangerous anywhere an attacker can influence the input: uploaded files, message queues, caches, model files, API bodies and configuration from other teams. The general class is covered in the Insecure Deserialisation Guide.

PatternWhy it is riskyBandit testSafer alternative
pickle.load, pickle.loadsThe Python documentation warns that malicious pickle data can execute arbitrary code during unpicklingB301 picklejson for untrusted data; if pickle is unavoidable, sign with hmac and verify before loading, or restrict globals with a custom Unpickler.find_class
marshalNot designed for untrusted inputB302 marshalJSON or a schema-based format
yaml.load with a full loaderPyYAML states it is as powerful as pickle.load and not safe with untrusted dataB506 yaml_loadyaml.safe_load (SafeLoader builds only standard YAML types)
eval, execRun arbitrary expressions or statementsB307 eval, B102 exec_usedExplicit parsing, a lookup table of allowed operations, or json.loads; ast.literal_eval only for trusted, size-limited input
subprocess with shell=TrueShell metacharacters in input run extra commandsB602Pass the command as a list of arguments; use shlex.quote only where a shell is truly required
String-built SQLSQL injectionB608Parameterised queries in the database driver or ORM
hashlib.md5 or sha1 for securityBroken for collision resistanceB324, B303SHA-256 or stronger; a password hashing function for passwords
random for tokensPredictable generatorB311secrets.token_urlsafe, secrets.token_bytes

Short safe examples

import json, yaml, subprocess, secrets

config = yaml.safe_load(untrusted_text)        # not yaml.load(...)
payload = json.loads(request_body)             # not pickle.loads(...)
subprocess.run(["convert", user_path, out_path], check=True)  # no shell=True
api_key = secrets.token_urlsafe(32)            # not random.choice(...)

Two points are often missed. First, ast.literal_eval does not run code, but the Python documentation now says a small input can exhaust memory or the C stack and crash the process, and recommends against calling it on untrusted data. Second, many machine learning model formats are pickle underneath, so loading a model file from an untrusted source is the same decision as unpickling it. See the AI Supply Chain and AI-BOM Guide for model provenance.

Pair these rules with input validation at every trust boundary (NIST SP 800-53 SI-10) and with the language-neutral practices in the Secure Coding Guide and the OWASP Top 10 Guide.

Startup hooks, bytecode and the interpreter environment

  • .pth files: the site module processes every .pth file in site-packages at startup. Path lines are added to sys.path; lines starting with import are passed to exec(). That code runs in every Python process on the host, whether or not the package is ever imported. Python 3.15 deprecates import lines in favour of .start files (PEP 829), with later releases planned to ignore them, but current deployments still run them. ATT&CK tracks this as T1546.018 Python Startup Hooks.
  • Interpreter flags: -S disables the site module entirely; -s or PYTHONNOUSERSITE disables the per-user site directory. For tightly controlled jobs, -I (isolated mode) also ignores PYTHON* environment variables and the script's directory.
  • Write access: any account that can write to site-packages can run code in every future Python process. Build images so that site-packages is owned by root and the application runs as a different, unprivileged user.
  • Bytecode is not obfuscation: .pyc files are easy to decompile and keep string constants, including secrets that were in a local edit but never committed.

Secrets in Python code and artefacts

Python applications commonly need cloud keys, database passwords and LLM API keys. The failure modes are well known and repeat across incidents.

  • Hardcoded values: Bandit flags likely hardcoded passwords with B105 (string) and B106 (function argument). Treat these as review prompts; dedicated secret scanners find far more key formats.
  • .env files: convenient locally, but they end up in images, wheels and backups. Add .env to .gitignore and .dockerignore, and load production secrets from a secrets manager or workload identity at runtime.
  • Compiled and packaged artefacts: in the PyPI admin GitHub token leak, a token added to a local file was never committed but survived in a __pycache__ .pyc file that was built into a public Docker image, where it stayed for about 15 months. Exclude __pycache__ and *.pyc in .dockerignore, build from clean checkouts, and scan images and wheels before publishing.
  • Environment variables in CI: any step that runs third-party code can read the job's environment. Give the publishing secret only to the publishing step.

Use secrets for generating tokens and hmac.compare_digest when comparing them. See the Top 10 Secrets Management Mistakes.

Tooling: scanning, advisories and the PSRT

Tool or sourceWhat it doesWhere to run it
BanditStatic analysis of Python source for risky calls and patterns (B-coded tests above)Pre-commit and pull request; fail on high-severity, high-confidence findings first
pip-audit (PyPA)Checks installed packages or requirements files against the Python Packaging Advisory Database via PyPI, or OSV with -s osv; supports --require-hashes, --strict, --fix with --dry-run, and CycloneDX outputCI on every lockfile change and on a daily schedule
OSV-ScannerMatches lockfiles (requirements.txt, poetry.lock, Pipfile.lock, pdm.lock, uv.lock, pylock.toml) against osv.devMonorepos and mixed-language estates
Secret scannerDetects credentials in code, history, images and packagesPre-commit, push protection and artefact pipeline
Python Security Response Team (PSRT)Handles vulnerabilities in CPython and pip reported to [email protected]; advisories go to the security-announce list and the OSV-format psf/advisory-database; the PSF is a CVE Numbering AuthoritySubscribe to security-announce; feed the advisory database into scanners

Report a malicious package on PyPI to [email protected], as PyPI's incident guidance asks. Keep interpreters on supported CPython release lines so that security fixes are available. Prioritisation of the findings these tools produce is covered in the CWE Top 25 Guide.

How to implement: a phased rollout

  1. Inventory: list every Python service, job, notebook platform and internal package, with its interpreter version, dependency file and publishing method. List every PyPI token and who holds it.
  2. Observe first: run pip-audit, OSV-Scanner and Bandit in report-only mode for two to four weeks. Baseline the findings rather than blocking builds on day one.
  3. Lock and hash: generate hashed lockfiles for one pilot service, switch its CI to --require-hashes, and confirm that rebuilds are reproducible. Then extend team by team.
  4. Single index: route all installs through an internal proxy, remove --extra-index-url usage, and block direct egress to PyPI from build agents once the proxy is proven.
  5. Move publishing: configure trusted publishing with a protected environment for each package, publish one release, then revoke all API tokens for that project.
  6. Enforce: make high-confidence Bandit findings (B301, B307, B506, B602) and known-exploited or critical vulnerabilities blocking, with a documented exception process that expires.
  7. Harden runtime: read-only site-packages, non-root user, .dockerignore for caches and .env, and integrity monitoring for new .pth files.

Rollback: keep the previous unhashed requirements and pipeline definition in version control for one release cycle. If hash-checking blocks a legitimate platform wheel, add its hash with pip hash rather than turning the mode off. If trusted publishing fails during an urgent release, issue a short-lived project-scoped token, publish, and revoke it the same day.

Decision guidance

  • pip-tools vs Poetry, PDM or uv: pip-tools keeps plain requirements.txt and fits existing pip workflows; the newer tools manage environments and lockfiles together. Pick one per organisation so scanning and review are consistent.
  • Small teams: hashed lockfiles, trusted publishing, pip-audit and Bandit in CI, plus a hosted secret scanner, cover most of the risk without a private index.
  • Large organisations: add an internal proxy with new-version quarantine, a central banned-API rule set, signed internal packages with attestations, and an owner for every publishing identity.

Detection and measurement

  • Monitor: new .pth or sitecustomize.py files in site-packages on servers and runners; Python processes making outbound connections to unexpected hosts; uploads to your PyPI projects that have no matching tag, workflow run or attestation; and pip installs that reach the public index directly.
  • Log: CI job logs for every install and publish, PyPI project security history, and secret-scanner alerts, retained long enough to investigate exposures that last months.

Metrics that show the programme is working:

  1. Percentage of Python builds installing with --require-hashes from a lockfile.
  2. Number of live PyPI API tokens (target: zero for projects on trusted publishing).
  3. Percentage of published releases carrying attestations.
  4. Open critical or known-exploited dependency vulnerabilities and median days to fix.
  5. Count of B301, B307, B506 and B602 findings in main branches, trending to zero.
  6. Median time from secret-scanner alert to credential revocation.

Framework mapping

Control areaNIST SP 800-53 Rev. 5NIST CSF 2.0Other
Dependency intake and provenanceSR-3 Supply Chain Controls and Processes, SR-11 Component AuthenticityGV.SC-05CIS Controls v8.1 Control 16 (16.4, 16.5)
Artefact integrity (hashes, attestations)SI-7 Software, Firmware, and Information IntegrityPR.PS-06ATT&CK T1195.001, T1195.002
Secure coding and static analysisSA-11 Developer Testing and Evaluation, SA-15 Development Process, Standards, and Tools, SI-10 Information Input ValidationPR.PS-06ISO/IEC 27001:2022 A.8.25, A.8.28
Vulnerability scanningRA-5 Vulnerability Monitoring and ScanningID.RA-01ISO/IEC 27001:2022 A.8.8
Publishing tokens and secretsIA-5 Authenticator ManagementPR.AA-01ATT&CK T1552.001

Common failure patterns

  • Trusted publishing added, old token kept: in the Ultralytics PyPI compromise, after the GitHub Actions injection was fixed, the attacker uploaded two more malicious releases with a long-lived PyPI token that had never been revoked.
  • Publishing token visible to every CI step: in the LiteLLM PyPI package breach, a compromised security scanner running in CI read the PYPI_PUBLISH token from the environment, and version 1.82.8 shipped a litellm_init.pth file that ran a credential stealer in every Python process on the host.
  • Extra index URLs: mixing internal and public indexes lets a public package with an internal name win resolution.
  • Floating ranges in production: requests>=2 in a deployed service means each build may install something nobody reviewed.
  • Pickle as a cache or message format: a Redis cache or queue that stores pickled objects becomes a code execution path for anyone who can write to it.
  • Scanning only the repository: secrets in bytecode, wheels and image layers go unnoticed, as the PyPI admin token leak showed.

Practitioner checklist

  • Every deployable Python project has a fully pinned, hashed lockfile and CI installs with --require-hashes.
  • Builds use --only-binary :all: or build sdists only in an isolated job.
  • All installs go through one internal index or proxy; no --extra-index-url.
  • Every package you publish uses trusted publishing with a protected environment, and no PyPI API tokens remain.
  • Bandit rules for pickle, marshal, yaml.load, eval, exec and shell=True block merges unless an approved exception exists.
  • pip-audit or OSV-Scanner runs on every lockfile change and daily, with known-exploited issues fixed first.
  • Images exclude __pycache__, *.pyc and .env, and are scanned for secrets before push.
  • site-packages is read-only to the application user and new .pth files raise an alert.
  • Your team is subscribed to Python security-announce and runs supported CPython versions.

Standards and references

Related NHI Mgmt Group resources: Secure Coding Guide · Software Supply Chain Security Guide · Secrets Management Guide · GitHub and GitHub Actions Security Guide · Node.js Security Guide

PDF for subscribers

Take this guide with you

Every section, table and checklist in one PDF, with links to the sources and related guides. Subscribe to our weekly NHI & AI Identity Journal and we’ll email you a download link.

Free weekly newsletter

Subscribe to the NHI & AI Identity Journal

The latest on NHI and Agentic AI security – articles, research, breaches, news and events every week.

Bonus 33% off our NHI Course when you subscribe.

NHIMG Editorial Note
Written and reviewed by Lalit Choda, NHI Mgmt Group. Last updated 24 October 2026.
NHI Mgmt Group — the #1 independent authority on Non-Human Identity, IAM, and Agentic AI security. nhimg.org

PDF Downloads for Newsletter Subscribers

Get the guide

Enter your details and we’ll email you a download link.

The full guide is free to read online. The PDF is a thank you for subscribing.

Fill in all fields marked * and tick the PDF box to continue.