More hardening
All checks were successful
All checks were successful
This commit is contained in:
parent
d2a46394fe
commit
1148b34bce
4 changed files with 251 additions and 2 deletions
|
|
@ -1183,3 +1183,99 @@ def test_build_enroll_pyz_excludes_tests_and_caches_and_returns_sha(tmp_path: Pa
|
|||
assert "/tests/" not in n
|
||||
assert not leaf.startswith("test_")
|
||||
assert leaf != "conftest.py"
|
||||
|
||||
|
||||
def test_safe_extract_tar_rejects_too_many_members(tmp_path: Path, monkeypatch):
|
||||
import enroll.remote as r
|
||||
|
||||
monkeypatch.setattr(r, "_TAR_MAX_MEMBERS", 3)
|
||||
|
||||
bio = io.BytesIO()
|
||||
with tarfile.open(fileobj=bio, mode="w:gz") as tf:
|
||||
for i in range(10):
|
||||
ti = tarfile.TarInfo(name=f"f{i}")
|
||||
ti.size = 1
|
||||
tf.addfile(ti, io.BytesIO(b"x"))
|
||||
|
||||
bio.seek(0)
|
||||
with tarfile.open(fileobj=bio, mode="r:gz") as tf:
|
||||
with pytest.raises(RuntimeError, match="too many members"):
|
||||
r._safe_extract_tar(tf, tmp_path)
|
||||
|
||||
|
||||
def test_safe_extract_tar_rejects_oversized_member(tmp_path: Path, monkeypatch):
|
||||
import enroll.remote as r
|
||||
|
||||
monkeypatch.setattr(r, "_TAR_MAX_FILE_BYTES", 100)
|
||||
|
||||
bio = io.BytesIO()
|
||||
with tarfile.open(fileobj=bio, mode="w:gz") as tf:
|
||||
ti = tarfile.TarInfo(name="big")
|
||||
payload = b"x" * 500
|
||||
ti.size = len(payload)
|
||||
tf.addfile(ti, io.BytesIO(payload))
|
||||
|
||||
bio.seek(0)
|
||||
with tarfile.open(fileobj=bio, mode="r:gz") as tf:
|
||||
with pytest.raises(RuntimeError, match="too large"):
|
||||
r._safe_extract_tar(tf, tmp_path)
|
||||
|
||||
|
||||
def test_safe_extract_tar_rejects_aggregate_size_bomb(tmp_path: Path, monkeypatch):
|
||||
import enroll.remote as r
|
||||
|
||||
# Each member is under the per-file cap, but the sum exceeds the total cap.
|
||||
monkeypatch.setattr(r, "_TAR_MAX_TOTAL_BYTES", 1000)
|
||||
|
||||
bio = io.BytesIO()
|
||||
with tarfile.open(fileobj=bio, mode="w:gz") as tf:
|
||||
for i in range(10):
|
||||
ti = tarfile.TarInfo(name=f"f{i}")
|
||||
payload = b"x" * 300
|
||||
ti.size = len(payload)
|
||||
tf.addfile(ti, io.BytesIO(payload))
|
||||
|
||||
bio.seek(0)
|
||||
with tarfile.open(fileobj=bio, mode="r:gz") as tf:
|
||||
with pytest.raises(RuntimeError, match="uncompressed size exceeds"):
|
||||
r._safe_extract_tar(tf, tmp_path)
|
||||
|
||||
|
||||
def test_safe_extract_tar_rejects_excessive_path_depth(tmp_path: Path, monkeypatch):
|
||||
import enroll.remote as r
|
||||
|
||||
monkeypatch.setattr(r, "_TAR_MAX_PATH_DEPTH", 4)
|
||||
|
||||
deep = "/".join(f"d{i}" for i in range(10)) + "/file"
|
||||
bio = io.BytesIO()
|
||||
with tarfile.open(fileobj=bio, mode="w:gz") as tf:
|
||||
ti = tarfile.TarInfo(name=deep)
|
||||
ti.size = 1
|
||||
tf.addfile(ti, io.BytesIO(b"x"))
|
||||
|
||||
bio.seek(0)
|
||||
with tarfile.open(fileobj=bio, mode="r:gz") as tf:
|
||||
with pytest.raises(RuntimeError, match="too deeply nested"):
|
||||
r._safe_extract_tar(tf, tmp_path)
|
||||
|
||||
|
||||
def test_safe_extract_tar_allows_normal_bundle(tmp_path: Path):
|
||||
"""A normal, modestly-sized bundle still extracts cleanly under the caps."""
|
||||
import enroll.remote as r
|
||||
|
||||
bio = io.BytesIO()
|
||||
with tarfile.open(fileobj=bio, mode="w:gz") as tf:
|
||||
for name, data in (
|
||||
("state.json", b"{}"),
|
||||
("artifacts/etc_custom/etc/app.conf", b"k=1\n"),
|
||||
):
|
||||
ti = tarfile.TarInfo(name=name)
|
||||
ti.size = len(data)
|
||||
tf.addfile(ti, io.BytesIO(data))
|
||||
|
||||
bio.seek(0)
|
||||
with tarfile.open(fileobj=bio, mode="r:gz") as tf:
|
||||
r._safe_extract_tar(tf, tmp_path)
|
||||
|
||||
assert (tmp_path / "state.json").is_file()
|
||||
assert (tmp_path / "artifacts" / "etc_custom" / "etc" / "app.conf").is_file()
|
||||
|
|
|
|||
|
|
@ -371,3 +371,74 @@ def test_binary_keyring_allowlist_still_scans_for_private_key_material():
|
|||
dangerous._content_deny_reason("/etc/apt/keyrings/mykey.gpg", private_binary)
|
||||
is None
|
||||
)
|
||||
|
||||
|
||||
def test_authorization_assignment_form_is_denied():
|
||||
"""A populated Authorization/Proxy-Authorization credential is refused
|
||||
whether written HTTP-style with ':' or config-style with '='.
|
||||
|
||||
The header scanner previously only anchored on ':', so a config assignment
|
||||
such as ``Authorization = Bearer <token>`` (and ``proxy_authorization``/
|
||||
``http_authorization`` spellings) slipped through safe mode.
|
||||
"""
|
||||
pol = IgnorePolicy(dangerous=False)
|
||||
leaking = [
|
||||
b"Authorization = Bearer supersecretvalue\n",
|
||||
b"Authorization: Bearer supersecretvalue\n",
|
||||
b"authorization=Bearer abcdef123456\n",
|
||||
b"proxy_authorization = Basic abcdef123456\n",
|
||||
b"Proxy-Authorization: Bearer abcdef123456\n",
|
||||
b"http_authorization = Bearer abcdef123456\n",
|
||||
]
|
||||
for data in leaking:
|
||||
assert (
|
||||
pol._content_deny_reason("/etc/example.conf", data) == "sensitive_content"
|
||||
), data
|
||||
|
||||
|
||||
def test_underscore_and_camelcase_token_keys_are_denied():
|
||||
"""Token keys the general assignment pattern misses -- a leading-underscore
|
||||
npm token, a camelCase ``authToken``, or a ``bearerToken`` spelling -- are
|
||||
refused when they carry a populated value."""
|
||||
pol = IgnorePolicy(dangerous=False)
|
||||
leaking = [
|
||||
b"_authToken=npm_abcdefghijklmnopqrstuvwxyz\n",
|
||||
b"//registry.npmjs.org/:_authToken=npm_abcdefghijklmnop\n",
|
||||
b"authToken=abcdefghijklmnopqrstuvwxyz\n",
|
||||
b"bearerToken=abcdefghijklmnopqrstuvwxyz\n",
|
||||
b"bearer_token = abcdef123456\n",
|
||||
b"auth_token: ya29.aRealLookingToken\n",
|
||||
]
|
||||
for data in leaking:
|
||||
assert (
|
||||
pol._content_deny_reason("/etc/app/app.conf", data) == "sensitive_content"
|
||||
), data
|
||||
|
||||
|
||||
def test_authorization_and_token_keys_no_false_positives():
|
||||
"""The broadened Authorization/token-key rules must not fire on ordinary
|
||||
config: a scheme keyword + value is required for the header form, and a
|
||||
populated value is required for the token-key form, so boolean/numeric
|
||||
settings and value-less mentions stay collectable."""
|
||||
pol = IgnorePolicy(dangerous=False)
|
||||
allowed = [
|
||||
# Authorization-ish keys with no bearer/basic/... scheme + value.
|
||||
b"AuthorizationEnabled = true\n",
|
||||
b"authorization = true\n",
|
||||
b"my_authorization: somevalue\n",
|
||||
# Token-ish keys that are not populated credentials.
|
||||
b"authTokenEnabled = true\n",
|
||||
b"authTokenTimeout = 30\n",
|
||||
b"AuthorizedKeysFile .ssh/authorized_keys\n",
|
||||
]
|
||||
for data in allowed:
|
||||
assert pol._content_deny_reason("/etc/app/app.conf", data) is None, data
|
||||
|
||||
# --dangerous still collects everything.
|
||||
dangerous = IgnorePolicy(dangerous=True)
|
||||
assert (
|
||||
dangerous._content_deny_reason(
|
||||
"/etc/example.conf", b"Authorization = Bearer secret\n"
|
||||
)
|
||||
is None
|
||||
)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue