You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@tooling.apache.org by sb...@apache.org on 2026/05/15 15:37:23 UTC

(tooling-trusted-releases) branch sbp updated: Add a lint check for .pth files

This is an automated email from the ASF dual-hosted git repository.

sbp pushed a commit to branch sbp
in repository https://gitbox.apache.org/repos/asf/tooling-trusted-releases.git


The following commit(s) were added to refs/heads/sbp by this push:
     new 86d21445 Add a lint check for .pth files
86d21445 is described below

commit 86d2144539dc63884dac537356e6e87a652a7eb2
Author: Sean B. Palmer <se...@miscoranda.com>
AuthorDate: Fri May 15 16:37:03 2026 +0100

    Add a lint check for .pth files
---
 .pre-commit-config.yaml    |  8 ++++++
 Makefile                   |  3 ++
 atr/docs/code-policies.md  |  4 +++
 scripts/check_pth_files.py | 69 ++++++++++++++++++++++++++++++++++++++++++++++
 4 files changed, 84 insertions(+)

diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index 414e1b2e..bc145968 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -191,6 +191,14 @@ repos:
       pass_filenames: false
       always_run: true
 
+    - id: check-pth-files
+      name: check .pth files in the venv
+      description: Refuse unknown .pth files in .venv; Python runs their import lines at interpreter startup, so they are a supply-chain attack vector
+      entry: python3 -S scripts/check_pth_files.py
+      language: system
+      pass_filenames: false
+      always_run: true
+
     - id: check-models-imports
       name: check models imports
       description: Ensure that the models only import from approved packages
diff --git a/Makefile b/Makefile
index 5804516d..29c1e022 100644
--- a/Makefile
+++ b/Makefile
@@ -129,9 +129,11 @@ serve-local:
 
 sync:
 	uv sync --frozen --no-dev
+	python3 -S scripts/check_pth_files.py
 
 sync-all:
 	uv sync --frozen --all-groups
+	python3 -S scripts/check_pth_files.py
 
 unit:
 	sh tests/run-unit.sh
@@ -140,4 +142,5 @@ update-deps:
 	pre-commit autoupdate || :
 	uv lock --upgrade --exclude-newer "$$(date -u +%Y-%m-%dT%H:%M:%SZ)"
 	uv sync --frozen --all-groups
+	python3 -S scripts/check_pth_files.py
 	uv export --frozen --format requirements-txt --no-emit-project --no-header --no-hashes > pip-audit.requirements
diff --git a/atr/docs/code-policies.md b/atr/docs/code-policies.md
index 66872428..83a28c56 100644
--- a/atr/docs/code-policies.md
+++ b/atr/docs/code-policies.md
@@ -21,3 +21,7 @@ These policies cover security and other miscellaneous policies that describe how
 ### Tasks
 
 * Secret values must never be passed to tasks. This ensures that `Task` objects and results can be considered public.
+
+### Dependencies
+
+* Every `.pth` file installed into the project's virtual environment must appear in the allowlist of [`scripts/check_pth_files.py`](/ref/scripts/check_pth_files.py), enforced by `make sync` and pre-commit, because Python executes their `import` lines on every interpreter startup and they are a known supply-chain attack vector.
diff --git a/scripts/check_pth_files.py b/scripts/check_pth_files.py
new file mode 100755
index 00000000..03950718
--- /dev/null
+++ b/scripts/check_pth_files.py
@@ -0,0 +1,69 @@
+#!/usr/bin/env python3
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+import pathlib
+import sys
+import textwrap
+from typing import Final
+
+_ALLOWED: Final = frozenset(
+    {
+        ("_virtualenv.pth", b"import _virtualenv"),
+    }
+)
+
+_VENV: Final = pathlib.Path(".venv")
+
+
+def main() -> None:
+    sys.exit(_run())
+
+
+def _run() -> int:
+    if not _VENV.is_dir():
+        print(f"{_VENV}/ not present; skipping .pth check", file=sys.stderr)
+        return 0
+    seen: set[pathlib.Path] = set()
+    bad: list[pathlib.Path] = []
+    for path in sorted(_VENV.rglob("*.pth")):
+        resolved = path.resolve()
+        if resolved in seen:
+            continue
+        seen.add(resolved)
+        content = path.read_bytes()
+        if (path.name, content) in _ALLOWED:
+            continue
+        bad.append(path)
+    if not bad:
+        return 0
+    print("Unexpected .pth file(s) detected in .venv.", file=sys.stderr)
+    print("Python executes every '.pth' line beginning with 'import'", file=sys.stderr)
+    print("on interpreter startup, which makes .pth a known", file=sys.stderr)
+    print("supply-chain attack vector. Audit each file by hand before", file=sys.stderr)
+    print("allowlisting it in scripts/check_pth_files.py.", file=sys.stderr)
+    print(file=sys.stderr)
+    for path in bad:
+        rel = path.relative_to(_VENV)
+        body = textwrap.indent(path.read_bytes().decode("utf-8", errors="replace"), "    ")
+        print(f"  .venv/{rel}", file=sys.stderr)
+        print(body, file=sys.stderr)
+    return 1
+
+
+if __name__ == "__main__":
+    main()


---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@tooling.apache.org
For additional commands, e-mail: commits-help@tooling.apache.org