fix: Security- und Quality-Hardening aus Audit
- ipc.py: _recvall() Loop für fragmentierte Socket-Reads, 64 KiB Payload-Limit gegen OOM - greeter.py: GREETD_SOCK Validierung (absoluter Pfad + S_ISSOCK), Socket-Leak behoben (_close_greetd_sock), shlex.split() statt str.split() für Exec-Befehle, greetd-Fehlermeldungen auf 200 Zeichen begrenzt, get_style_context() durch get_color() ersetzt (GTK 4.10+ Deprecation), Socket-Timeout (10s) - users.py: ValueError-Absicherung bei int(uid_str), Username- Sanitierung gegen Pfad-Traversal, Symlink-Check bei Avatar-Pfaden
This commit is contained in:
@@ -42,6 +42,27 @@ class FakeSocket:
|
||||
return cls(recv_data=data)
|
||||
|
||||
|
||||
class FragmentingSocket:
|
||||
"""A fake socket that delivers data in small chunks to simulate fragmentation."""
|
||||
|
||||
def __init__(self, data: bytes, chunk_size: int = 3):
|
||||
self.sent = bytearray()
|
||||
self._data = data
|
||||
self._offset = 0
|
||||
self._chunk_size = chunk_size
|
||||
|
||||
def sendall(self, data: bytes) -> None:
|
||||
self.sent.extend(data)
|
||||
|
||||
def recv(self, n: int, flags: int = 0) -> bytes:
|
||||
available = min(n, self._chunk_size, len(self._data) - self._offset)
|
||||
if available <= 0:
|
||||
return b""
|
||||
chunk = self._data[self._offset : self._offset + available]
|
||||
self._offset += available
|
||||
return chunk
|
||||
|
||||
|
||||
class TestSendMessage:
|
||||
"""Tests for encoding and sending length-prefixed JSON messages."""
|
||||
|
||||
@@ -107,6 +128,25 @@ class TestRecvMessage:
|
||||
with pytest.raises(ConnectionError):
|
||||
recv_message(sock)
|
||||
|
||||
def test_receives_fragmented_data(self) -> None:
|
||||
"""recv() may return fewer bytes than requested — must loop."""
|
||||
response = {"type": "success"}
|
||||
payload = json.dumps(response).encode("utf-8")
|
||||
data = struct.pack("!I", len(payload)) + payload
|
||||
sock = FragmentingSocket(data, chunk_size=3)
|
||||
|
||||
result = recv_message(sock)
|
||||
|
||||
assert result == response
|
||||
|
||||
def test_rejects_oversized_payload(self) -> None:
|
||||
"""Payloads exceeding the size limit must be rejected."""
|
||||
header = struct.pack("!I", 10_000_000)
|
||||
sock = FakeSocket(recv_data=header)
|
||||
|
||||
with pytest.raises(ConnectionError, match="too large"):
|
||||
recv_message(sock)
|
||||
|
||||
|
||||
class TestCreateSession:
|
||||
"""Tests for the create_session greetd request."""
|
||||
|
||||
@@ -61,6 +61,32 @@ class TestGetUsers:
|
||||
assert users[0].gecos == ""
|
||||
assert users[0].display_name == "user"
|
||||
|
||||
def test_skips_invalid_uid(self, tmp_path: Path) -> None:
|
||||
"""Corrupt /etc/passwd with non-numeric UID should not crash."""
|
||||
passwd = tmp_path / "passwd"
|
||||
passwd.write_text(
|
||||
"corrupt:x:NOTANUMBER:1000:Corrupt:/home/corrupt:/bin/bash\n"
|
||||
"valid:x:1000:1000:Valid:/home/valid:/bin/bash\n"
|
||||
)
|
||||
|
||||
users = get_users(passwd_path=passwd)
|
||||
|
||||
assert len(users) == 1
|
||||
assert users[0].username == "valid"
|
||||
|
||||
def test_skips_username_with_slash(self, tmp_path: Path) -> None:
|
||||
"""Usernames containing path separators should be rejected."""
|
||||
passwd = tmp_path / "passwd"
|
||||
passwd.write_text(
|
||||
"../evil:x:1000:1000:Evil:/home/evil:/bin/bash\n"
|
||||
"normal:x:1001:1001:Normal:/home/normal:/bin/bash\n"
|
||||
)
|
||||
|
||||
users = get_users(passwd_path=passwd)
|
||||
|
||||
assert len(users) == 1
|
||||
assert users[0].username == "normal"
|
||||
|
||||
|
||||
class TestGetAvatarPath:
|
||||
"""Tests for avatar file lookup."""
|
||||
@@ -88,6 +114,22 @@ class TestGetAvatarPath:
|
||||
|
||||
assert result == face
|
||||
|
||||
def test_ignores_symlinked_face(self, tmp_path: Path) -> None:
|
||||
"""~/.face as symlink should be ignored to prevent traversal."""
|
||||
home = tmp_path / "home" / "attacker"
|
||||
home.mkdir(parents=True)
|
||||
target = tmp_path / "secret.txt"
|
||||
target.write_text("sensitive data")
|
||||
face = home / ".face"
|
||||
face.symlink_to(target)
|
||||
empty_icons = tmp_path / "no_icons"
|
||||
|
||||
result = get_avatar_path(
|
||||
"attacker", accountsservice_dir=empty_icons, home_dir=home
|
||||
)
|
||||
|
||||
assert result is None
|
||||
|
||||
def test_returns_none_when_no_avatar(self, tmp_path: Path) -> None:
|
||||
empty_icons = tmp_path / "no_icons"
|
||||
home = tmp_path / "home" / "nobody"
|
||||
|
||||
Reference in New Issue
Block a user