From bb1bc0325b4367afd4c6da0684d5d20c6f2121bb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bern=C3=A1t=20G=C3=A1bor?= Date: Wed, 28 Oct 2026 00:47:09 -0700 Subject: [PATCH 1/2] =?UTF-8?q?=F0=9F=93=9D=20docs(security):=20add=20a=20?= =?UTF-8?q?threat=20model=20and=20align=20the=20policy?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/SECURITY.md | 56 ++++++++++++++++++++------- .github/THREAT_MODEL.md | 86 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 127 insertions(+), 15 deletions(-) create mode 100644 .github/THREAT_MODEL.md diff --git a/.github/SECURITY.md b/.github/SECURITY.md index 258451e..f96c9e9 100644 --- a/.github/SECURITY.md +++ b/.github/SECURITY.md @@ -1,23 +1,49 @@ -# Reporting Vulnerabilities +# Security policy -**⚠️ Please do not file public GitHub issues for security vulnerabilities as they are open for everyone to see! ⚠️** +pytest-env is a pytest plugin that sets environment variables for a test session from pytest configuration and `.env` +files. This policy says how to report a security problem and how we decide whether a report is one. -We encourage responsible disclosure practices for security vulnerabilities. +## Reporting a vulnerability -## Reporting a Vulnerability +**Do not open a public issue for a security problem.** Use +[GitHub's private vulnerability reporting](https://github.com/pytest-dev/pytest-env/security/advisories/new). If you do +not have a GitHub account, or you are a Tidelift subscriber, report through the +[Tidelift security contact](https://tidelift.com/security) instead; Tidelift coordinates the fix and the disclosure with +us. If neither works for you, email [gaborjbernat@gmail.com](mailto:gaborjbernat@gmail.com). -If you believe you've found a security-related bug, fill out a new vulnerability report via GitHub directly. To do so, -follow these instructions: +Include what the attacker controls and what they gain, the steps or a proof of concept, and the versions and platform +you tested. Write the report yourself; unreviewed tool or model output takes longer to triage. -1. Click on the `Security` tab in the project repository. -1. Click the green `Report a vulnerability` button at the top right corner. -1. Fill in the form as accurately as you can, including as many details as possible. -1. Click the green `Submit report` button at the bottom. +## What to expect -## Don't have a GitHub account? +Volunteers maintain pytest-env, with no on-call rotation. We aim to acknowledge a report within **5 working days** and +assess it within **15 working days**. Fixes are developed in a private fork attached to the draft advisory, ship in a +new release, and the advisory, with a CVE requested through GitHub, goes out with that release. The default embargo is +**90 days** from the report; we agree any change with the reporter. We credit reporters in the advisory under the name +they choose, or not at all. There is no bug bounty. -Alternatively, to report a security vulnerability, please use the -[Tidelift security contact](https://tidelift.com/security). Tidelift will coordinate the fix and disclosure. +## Supported versions -It is currently set up to forward every incoming report to Bernát Gábor. We will try to assess the problem in timely -manner and disclose it in a responsible way. +Only the latest release receives fixes. Confirm the problem reproduces on it before reporting. + +## Vulnerability or bug + +A report is a vulnerability when an attacker who controls only an input that the [threat model](THREAT_MODEL.md) lists +as untrusted breaks a guarantee pytest-env makes. A report that first needs control of something the threat model +trusts, such as the pytest configuration or the `.env` files it names, is a bug: we fix it in the open and publish no +advisory. Classify by who supplies the dangerous input, not by how bad the outcome sounds. + +**In scope** + +- pytest-env loading a `.env` file the configuration or `--envfile` did not name, or one outside the rootdir for a + relative name. +- A variable value printed when `--pytest-env-verbose` is off. + +**Out of scope** + +- Values the configuration sets: the project's pytest configuration is trusted, like the tests it runs. +- `{VAR}` expansion reaching other variables: it is documented, and `R:` turns it off. +- Values printed with `--pytest-env-verbose`, which exists to print them. +- Parsing behavior of `.env` files, which belongs to python-dotenv. + +If you cannot tell which side a finding falls on, report it privately and we will decide. diff --git a/.github/THREAT_MODEL.md b/.github/THREAT_MODEL.md new file mode 100644 index 0000000..e8afc6e --- /dev/null +++ b/.github/THREAT_MODEL.md @@ -0,0 +1,86 @@ +# Threat model + +This model says what pytest-env trusts, where untrusted input enters, and which guarantees hold at each boundary, so a +report can be classified as a vulnerability or a bug. [SECURITY.md](SECURITY.md) covers reporting; if the two disagree, +SECURITY.md wins and we fix this file. + +## How pytest-env works + +Before tests are collected, pytest-env reads `env` entries from `pytest.toml`, `pyproject.toml` or `pytest.ini`, and +loads the `.env` files that `env_files` or `--envfile` name, resolved against the rootdir and parsed with python-dotenv. +It sets, unsets or keeps each variable, expanding `{VAR}` references unless the entry is raw. With +`--pytest-env-verbose` it prints each assignment, values included, in the report header. + +```mermaid +flowchart LR + cfg[("pytest config
(trusted)")] --> pe["pytest-env"] + dot[(".env files named by config
(trusted)")] --> pe + pe -->|"set, unset, expand"| env["os.environ for the session"] + pe -->|"--pytest-env-verbose only"| log["Report header"] + classDef trusted fill:#cfe4ff,stroke:#1f6feb,color:#0b1f3a; + classDef boundary fill:#ffe3a3,stroke:#d29200,color:#3a2c00; + class cfg,dot,pe trusted; + class env,log boundary; +``` + +## Compared with similar tools + +We drew the boundary where comparable projects in other ecosystems draw theirs. The shared rule, stated by the +[Node.js threat model](https://github.com/nodejs/node/blob/main/SECURITY.md#the-nodejs-threat-model), is that a report +which first needs a trusted element compromised is out of scope; the first triage question, as +[DOMPurify](https://github.com/cure53/DOMPurify/blob/main/SECURITY.md#what-we-consider-a-vulnerability) puts it, is what +the attacker controls. + +| Project | Language | Where it draws the line | +| -------------------------------------------------------------- | ---------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| [direnv](https://direnv.net/man/direnv.1.html) | Go | Treats `.envrc` as untrusted until `direnv allow`, because it executes it; pytest-env's `.env` files are data named by trusted configuration and are never executed. | +| [dotenv](https://github.com/motdotla/dotenv/security/policy) | JavaScript | Parses `.env` files as data; publishes a reporting policy only. | +| [Vitest](https://github.com/vitest-dev/vitest/security/policy) | JavaScript | Trusts the developer, the config file, plugins and every project file; only reports that need no trusted element compromised count. | +| [Jest](https://github.com/jestjs/jest/security/policy) | JavaScript | Test runner; private reports with an escalation path, and no separate threat model. | + +## Trusted + +An attacker who controls any of these already has what pytest-env could give them: + +- The pytest configuration and command line, including `env`, `env_files` and `--envfile`. +- The `.env` files they name, and the process environment used for `{VAR}` expansion. +- The tests, which run with the variables set. + +## Untrusted inputs + +| Input | Comes from | What pytest-env guarantees | +| ------------------------- | ------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------- | +| Variable values in output | Secrets placed in `.env` files | Not printed unless `--pytest-env-verbose` is given. | +| Relative `.env` paths | Configuration | Resolved against the pytest rootdir and never searched for elsewhere; a missing `--envfile` raises, a missing configured file is skipped. | + +## Threats and controls + +| STRIDE | Threat | Control | +| ---------------------- | ------------------------------------------------ | ---------------------------------------------------- | +| Information disclosure | A secret from `.env` appears in a public CI log. | Values are printed only with `--pytest-env-verbose`. | +| Tampering | A `.env` file from another directory is loaded. | Only named files, resolved against the rootdir. | + +## Release chain + +Releases run on the shared [tox-dev/github-workflows](https://github.com/tox-dev/github-workflows) pipeline. A release +App, not a person, pushes the release commit and tag; the build installs its tools hash-checked from `pylock.toml` with +a seven-day upload cooldown; PyPI receives the files through +[trusted publishing](https://docs.pypi.org/trusted-publishers/) with attestations, and the GitHub Release is immutable. +Every workflow pins third-party actions to a commit SHA. + +## Vulnerability or bug + +| Report | Verdict | Why | +| --------------------------------------------------------------------- | ------------------- | ----------------------------- | +| A secret shows up in the report header without `--pytest-env-verbose` | Vulnerability | Output guarantee broken. | +| `FOO = "{HOME}"` expands to the home directory | Not a vulnerability | Documented expansion. | +| A malicious `pytest.ini` sets `LD_PRELOAD` | Not a vulnerability | The configuration is trusted. | + +## Accepted risks + +- `--pytest-env-verbose` prints values, secrets included; do not enable it with secrets in public logs. + +## Keeping this current + +Revisit this model when pytest-env gains a new input source, network access, a new way to run code, or a new place it +writes to, and whenever an advisory is published. From 472c6bbfae05e6ee76bb87d8190d8b69424cc0c7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bern=C3=A1t=20G=C3=A1bor?= Date: Wed, 28 Oct 2026 07:09:35 -0700 Subject: [PATCH 2/2] =?UTF-8?q?=F0=9F=93=9D=20docs(security):=20check=20th?= =?UTF-8?q?e=20threat=20model=20against=20the=20code?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/SECURITY.md | 13 +++++++++---- .github/THREAT_MODEL.md | 43 ++++++++++++++++++++++++++--------------- 2 files changed, 36 insertions(+), 20 deletions(-) diff --git a/.github/SECURITY.md b/.github/SECURITY.md index f96c9e9..1798914 100644 --- a/.github/SECURITY.md +++ b/.github/SECURITY.md @@ -35,14 +35,19 @@ advisory. Classify by who supplies the dangerous input, not by how bad the outco **In scope** -- pytest-env loading a `.env` file the configuration or `--envfile` did not name, or one outside the rootdir for a - relative name. +- pytest-env loading a `.env` file that neither the configuration nor `--envfile` names. - A variable value printed when `--pytest-env-verbose` is off. +- TOML, INI and command-line precedence, or the skip-if-set baseline, differing from the documentation so a value the + user protected is overwritten. +- A configured value creating or removing a variable other than the one its entry names. **Out of scope** -- Values the configuration sets: the project's pytest configuration is trusted, like the tests it runs. -- `{VAR}` expansion reaching other variables: it is documented, and `R:` turns it off. +- Values the configuration sets: the project's pytest configuration is trusted, like the `conftest.py` and tests it + runs. +- `{VAR}` expansion reaching other variables: it is documented, off with `R:` in INI and opt-in with `transform` in + TOML. +- `.env` paths the configuration or `--envfile` chooses, including ones outside the rootdir. - Values printed with `--pytest-env-verbose`, which exists to print them. - Parsing behavior of `.env` files, which belongs to python-dotenv. diff --git a/.github/THREAT_MODEL.md b/.github/THREAT_MODEL.md index e8afc6e..2afe894 100644 --- a/.github/THREAT_MODEL.md +++ b/.github/THREAT_MODEL.md @@ -6,10 +6,12 @@ SECURITY.md wins and we fix this file. ## How pytest-env works -Before tests are collected, pytest-env reads `env` entries from `pytest.toml`, `pyproject.toml` or `pytest.ini`, and -loads the `.env` files that `env_files` or `--envfile` name, resolved against the rootdir and parsed with python-dotenv. -It sets, unsets or keeps each variable, expanding `{VAR}` references unless the entry is raw. With -`--pytest-env-verbose` it prints each assignment, values included, in the report header. +Before tests are collected, pytest-env reads its entries from the `[pytest_env]` table of `pytest.toml`, `.pytest.toml` +or `pyproject.toml`, found in the pytest configuration's directory or the nearest parent with such a table, or else from +the `env` option of the INI configuration. It loads the `.env` files that `env_files` or `--envfile` name, resolved +against the rootdir and parsed with python-dotenv, then sets, unsets or keeps each variable, expanding `{VAR}` +references in INI entries not marked `R:` and in TOML entries that set `transform`. With `--pytest-env-verbose` it +prints each assignment, values included, in the report header. ```mermaid flowchart LR @@ -34,7 +36,7 @@ the attacker controls. | Project | Language | Where it draws the line | | -------------------------------------------------------------- | ---------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | [direnv](https://direnv.net/man/direnv.1.html) | Go | Treats `.envrc` as untrusted until `direnv allow`, because it executes it; pytest-env's `.env` files are data named by trusted configuration and are never executed. | -| [dotenv](https://github.com/motdotla/dotenv/security/policy) | JavaScript | Parses `.env` files as data; publishes a reporting policy only. | +| [dotenv](https://github.com/motdotla/dotenv/security/policy) | JavaScript | Publishes only a reporting contact. | | [Vitest](https://github.com/vitest-dev/vitest/security/policy) | JavaScript | Trusts the developer, the config file, plugins and every project file; only reports that need no trusted element compromised count. | | [Jest](https://github.com/jestjs/jest/security/policy) | JavaScript | Test runner; private reports with an escalation path, and no separate threat model. | @@ -48,23 +50,24 @@ An attacker who controls any of these already has what pytest-env could give the ## Untrusted inputs -| Input | Comes from | What pytest-env guarantees | -| ------------------------- | ------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------- | -| Variable values in output | Secrets placed in `.env` files | Not printed unless `--pytest-env-verbose` is given. | -| Relative `.env` paths | Configuration | Resolved against the pytest rootdir and never searched for elsewhere; a missing `--envfile` raises, a missing configured file is skipped. | +| Input | Comes from | What pytest-env guarantees | +| ------------------------- | --------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------- | +| Variable values in output | Secrets placed in `.env` files or the configuration | Not printed unless `--pytest-env-verbose` is given. | +| `.env` names | Configuration and `--envfile` | Resolved against the rootdir and never searched for elsewhere; a missing `--envfile` raises, a missing configured file is skipped. | ## Threats and controls -| STRIDE | Threat | Control | -| ---------------------- | ------------------------------------------------ | ---------------------------------------------------- | -| Information disclosure | A secret from `.env` appears in a public CI log. | Values are printed only with `--pytest-env-verbose`. | -| Tampering | A `.env` file from another directory is loaded. | Only named files, resolved against the rootdir. | +| STRIDE | Threat | Control | +| ---------------------- | ------------------------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Information disclosure | A secret from `.env` appears in a public CI log. | Values are printed only with `--pytest-env-verbose`. | +| Tampering | A `.env` file nobody named is loaded. | Only named files are loaded. | +| Tampering | A protected variable is overwritten. | `env_files_skip_if_set` takes its baseline before any `.env` file is applied; `D:` and `skip_if_set` entries check the environment at their place in the list. | ## Release chain Releases run on the shared [tox-dev/github-workflows](https://github.com/tox-dev/github-workflows) pipeline. A release -App, not a person, pushes the release commit and tag; the build installs its tools hash-checked from `pylock.toml` with -a seven-day upload cooldown; PyPI receives the files through +App, not a person, pushes the release commit and tag; the build installs its tools from a hash-checked `pylock.toml` +resolved with a seven-day upload cooldown by default; PyPI receives the files through [trusted publishing](https://docs.pypi.org/trusted-publishers/) with attestations, and the GitHub Release is immutable. Every workflow pins third-party actions to a commit SHA. @@ -73,12 +76,20 @@ Every workflow pins third-party actions to a commit SHA. | Report | Verdict | Why | | --------------------------------------------------------------------- | ------------------- | ----------------------------- | | A secret shows up in the report header without `--pytest-env-verbose` | Vulnerability | Output guarantee broken. | -| `FOO = "{HOME}"` expands to the home directory | Not a vulnerability | Documented expansion. | +| The INI entry `FOO={HOME}` expands to the home directory | Not a vulnerability | Documented expansion. | | A malicious `pytest.ini` sets `LD_PRELOAD` | Not a vulnerability | The configuration is trusted. | +## Severity + +- **High:** a secret value printed without `--pytest-env-verbose`. +- **Medium:** an unnamed `.env` file loaded, or a protected variable overwritten against the documented precedence. +- **Low:** a crash or confusing error on malformed configuration. + ## Accepted risks - `--pytest-env-verbose` prints values, secrets included; do not enable it with secrets in public logs. +- The TOML search walks up parent directories without checking who owns them; do not run pytest below a directory other + users can write. ## Keeping this current