Skip to content

fix: contain registry path resolution against traversal (F-05-01)#11

Merged
kurtseifried merged 1 commit into
mainfrom
fix/path-traversal-containment
Jun 21, 2026
Merged

fix: contain registry path resolution against traversal (F-05-01)#11
kurtseifried merged 1 commit into
mainfrom
fix/path-traversal-containment

Conversation

@kurtseifried

Copy link
Copy Markdown
Contributor

Finding

F-05-01 (HIGH) from the 2026-06 security audit. registry_loader.load_single() builds a filesystem path from the untrusted secid query (Path(registry_dir)/<type>/<reversed-dns>/<subpath>.json) with no containment. pathlib's / join does not collapse .., so:

?secid=advisory/x/../../../../etc/passwd  →  /etc/passwd.json

escapes the registry tree — an arbitrary-.json read (e.g. other tenants' private overlays) plus a filesystem existence/readability oracle. Remote-unauthenticated on the shipped --host 0.0.0.0 / no-auth defaults, on the default lazy-load path.

Fix (two layers, fail closed)

  • _reject_unsafe_segment() — rejects .. path segments, absolute paths, backslashes, and NUL at the resolver boundary (_match_namespace) and in load_single/load_type_info. Returns (None, None) / None, which flows into the existing not-found path (no error oracle).
  • _contained_path() — after building the path, resolve()s both it and the registry root and requires containment; applied at all three filesystem sinks (load_single, build_type_index, load_type_info). Fails closed on escape/resolution error.

Legitimate dotted namespaces (redhat.com, legislation.gov.uk) are unaffected — only .. segments are rejected, not dots within a label.

Tests

4 regression tests added (_reject_unsafe_segment, _contained_path, load_single traversal block, end-to-end resolve not-found + secret-never-read). Full suite: 32 passed.

Notes

  • Also closes the latent sibling F-05-02 (the build_type_index/load_type_info joins) via the same _contained_path guard.
  • Backward-compatible: no API/behavior change for valid queries.

Generated from the audit patch PATCHES/01-path-traversal-containment.patch.md.

🤖 Generated with Claude Code

load_single() built Path(registry_dir)/secid_type/<reversed-dns>/<subpath>.json
from the untrusted secid query with no containment; pathlib's '/' join does not
collapse '..', so ?secid=advisory/x/../../../../etc/passwd escaped the registry
tree (arbitrary .json read + existence oracle), remote-unauthenticated on the
shipped 0.0.0.0/no-auth defaults.

Two layers, fail closed:
- _reject_unsafe_segment(): refuse '..'/absolute/backslash/NUL segments at the
  resolver boundary (_match_namespace) and in load_single/load_type_info.
- _contained_path(): after building the path, resolve() both it and the
  registry root and require containment; applied at all three filesystem sinks
  (load_single, build_type_index, load_type_info).

Legitimate dotted namespaces (redhat.com) are unaffected — only '..' path
segments are rejected. Adds 4 regression tests; full suite 32 passed.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Comment thread python/registry_loader.py
"""
try:
base = Path(registry_dir).resolve()
target = full_path.resolve()
Comment thread python/registry_loader.py
# Defense in depth: confirm the joined path stays inside the registry
# tree even if a hostile segment slipped past the check above.
full_path = _contained_path(registry_dir, Path(registry_dir) / secid_type / fs_path)
if full_path and full_path.exists():
Comment thread python/registry_loader.py Outdated
@kurtseifried kurtseifried merged commit b8bf3e2 into main Jun 21, 2026
6 of 7 checks passed
@kurtseifried kurtseifried deleted the fix/path-traversal-containment branch June 21, 2026 04:52
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.

2 participants