Skip to content

security: harden release artifact validation - #495

Open
codeforester wants to merge 3 commits into
mainfrom
security/486-20261028-artifact-content-checks
Open

codeforester wants to merge 3 commits into
mainfrom
security/486-20261028-artifact-content-checks

Conversation

@codeforester

Copy link
Copy Markdown
Contributor

Summary

  • reject duplicate, traversal, startup-hook, symlink, executable, and native-library archive members
  • compare packaged base_cli files byte-for-byte with the reviewed source tree
  • add malicious archive fixtures for duplicate members, traversal, and startup hooks
  • preserve legitimate executable Python files under scripts/ in sdists

Security rationale

The release validator now fails closed before installation. It checks both wheel and sdist content and does not import or execute the artifact while validating it.

Validation

  • UV_CACHE_DIR=/private/tmp/base-cli-uv-cache uv run --extra dev pytest -q tests/test_validate_package_artifact.py
  • UV_CACHE_DIR=/private/tmp/base-cli-uv-cache uv run --extra dev python -m build --wheel --sdist
  • UV_CACHE_DIR=/private/tmp/base-cli-uv-cache uv run --extra dev python scripts/validate_package_artifact.py dist
  • Ruff and git diff --check pass

Relates to #486.

@codeforester
codeforester force-pushed the security/486-20261028-artifact-content-checks branch from c8541ec to 3660261 Compare October 27, 2026 18:53
Comment thread scripts/validate_package_artifact.py
Comment thread scripts/validate_package_artifact.py
Comment thread scripts/validate_package_artifact.py Outdated
relative = name[len(root_prefix) :]
if relative not in ALLOWED_SDIST_FILES and not relative.startswith(ALLOWED_SDIST_PREFIXES):
fail(f"{path.name} contains non-allowlisted archive member {relative!r}")
package_prefix = "lib/python/base_cli/"

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Robustness: this hardcodes the package path prefix as the literal string "lib/python/base_cli/" instead of deriving it from the IMPORT_NAME constant used everywhere else in this file. If IMPORT_NAME is ever changed, every other call site updates automatically via the constant, but this literal silently stops matching any real package file — relative.startswith(package_prefix) then never fires, and the new sha256 hash-verification control for sdist package files silently stops applying to anything, with no error raised.

@@ -64,7 +67,40 @@ def expected_package_files() -> set[str]:
package_root = Path(__file__).resolve().parents[1] / "lib" / "python" / IMPORT_NAME

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cleanup (dead code / drift risk): expected_package_files() is now dead — main() only calls expected_package_hashes() and derives the files set from its keys — but expected_package_hashes() re-implements the identical rglob/filter traversal from scratch instead of reusing expected_package_files(). Both copies independently skip __pycache__/.pyc; a future edit to that filtering rule applied to only one copy would silently desync the file-allowlist from the hash-allowlist, and the unused function remains as a misleading signal that it's still part of the validation path.

@codeforester

Copy link
Copy Markdown
Contributor Author

Implemented the review fixes in commit 703267a2.

  • Wheel directory entries are skipped before executable-bit validation, so legitimate directory modes do not fail artifact validation.
  • Wheel symbolic links are rejected explicitly using the Unix file-type bits, matching the sdist hard-link/symlink policy.
  • Added regression tests for both behaviors.

@codeforester

Copy link
Copy Markdown
Contributor Author

Addressed the remaining lint and maintainability feedback in commit dc117eb6.

  • Replaced the UTF-8 string .encode() with a bytes literal (UP012).
  • Removed the dead expected_package_files() traversal and made expected_package_hashes() the single source of truth.
  • Derived the sdist package prefixes from IMPORT_NAME instead of duplicating base_cli literals.

The directory-entry and symlink regression tests remain in place.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant