Merge pull request #234 from living-ip/fix/iap-extracted-member-modes
Some checks are pending
CI / lint-and-test (push) Waiting to run

Preserve IAP request member modes
This commit is contained in:
Fawaz 2026-07-22 22:37:28 -07:00 committed by GitHub
commit 508e60edfb
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 42 additions and 0 deletions

View file

@ -272,15 +272,32 @@ def _acquire_lock(path: Path = LOCK_PATH, *, expected_uid: int = 0) -> int:
def _copy_into_request(path: Path, content: bytes, mode: int = 0o600) -> None: def _copy_into_request(path: Path, content: bytes, mode: int = 0o600) -> None:
_require(mode in {0o600, 0o644, 0o755}, "request file mode was invalid")
flags = os.O_WRONLY | os.O_CREAT | os.O_EXCL flags = os.O_WRONLY | os.O_CREAT | os.O_EXCL
if hasattr(os, "O_NOFOLLOW"): if hasattr(os, "O_NOFOLLOW"):
flags |= os.O_NOFOLLOW flags |= os.O_NOFOLLOW
try: try:
descriptor = os.open(path, flags, mode) descriptor = os.open(path, flags, mode)
with os.fdopen(descriptor, "wb") as handle: with os.fdopen(descriptor, "wb") as handle:
# receive_request() deliberately runs under umask 077. Apply the
# manifest-bound mode explicitly so public release metadata and
# executable helpers retain their reviewed 0644/0755 posture.
os.fchmod(handle.fileno(), mode)
handle.write(content) handle.write(content)
handle.flush() handle.flush()
os.fsync(handle.fileno()) os.fsync(handle.fileno())
observed = os.fstat(handle.fileno())
named = path.stat(follow_symlinks=False)
_require(
stat.S_ISREG(observed.st_mode)
and (observed.st_dev, observed.st_ino) == (named.st_dev, named.st_ino)
and observed.st_uid == os.geteuid()
and stat.S_IMODE(observed.st_mode) == mode
and stat.S_IMODE(named.st_mode) == mode
and observed.st_nlink == 1
and observed.st_size == len(content),
"request file posture differed from its manifest",
)
except OSError as exc: except OSError as exc:
raise RemoteError("request file could not be secured") from exc raise RemoteError("request file could not be secured") from exc

View file

@ -284,6 +284,31 @@ def test_archive_is_deterministic_and_extracts_exact_members(tmp_path: Path) ->
) )
def test_archive_extraction_preserves_manifest_modes_under_restrictive_umask(tmp_path: Path) -> None:
archive_root = private_directory(tmp_path / "archive")
extracted = private_directory(tmp_path / "extracted")
allowed_modes = (0o600, 0o644, 0o755)
payloads = {
name: (f"payload:{name}\n".encode(), allowed_modes[index % len(allowed_modes)])
for index, name in enumerate(sorted(remote.EXPECTED_MEMBERS - {"request-manifest.json"}))
}
manifest = archive_manifest(payloads)
archive = archive_root / "request.tar.gz"
local.build_archive(archive, manifest=manifest, payloads=payloads)
previous_umask = os.umask(0o077)
try:
remote.extract_archive(archive, extracted)
finally:
os.umask(previous_umask)
assert {
str(path.relative_to(extracted)): stat.S_IMODE(path.stat().st_mode)
for path in extracted.rglob("*")
if path.is_file()
} == {name: mode for name, (_content, mode) in payloads.items()}
def write_unsafe_archive(path: Path, *, kind: str) -> None: def write_unsafe_archive(path: Path, *, kind: str) -> None:
with tarfile.open(path, "w:gz") as archive: with tarfile.open(path, "w:gz") as archive:
for index, name in enumerate(sorted(remote.EXPECTED_MEMBERS)): for index, name in enumerate(sorted(remote.EXPECTED_MEMBERS)):