diff --git a/src/moongreet/users.py b/src/moongreet/users.py index 358c0c1..cf1f508 100644 --- a/src/moongreet/users.py +++ b/src/moongreet/users.py @@ -2,9 +2,12 @@ # ABOUTME: Provides User dataclass and helper functions for the greeter UI. import configparser +import re from dataclasses import dataclass from pathlib import Path +VALID_THEME_NAME = re.compile(r"^[A-Za-z0-9_-]+$") + NOLOGIN_SHELLS = {"/usr/sbin/nologin", "/sbin/nologin", "/bin/false", "/usr/bin/nologin"} MIN_UID = 1000 MAX_UID = 65533 @@ -102,6 +105,9 @@ def get_user_gtk_theme(config_dir: Path | None = None) -> str | None: return None if config.has_option("Settings", "gtk-theme-name"): - return config.get("Settings", "gtk-theme-name") + theme = config.get("Settings", "gtk-theme-name") + # Validate against path traversal — only allow safe theme names + if theme and VALID_THEME_NAME.match(theme): + return theme return None diff --git a/tests/test_integration.py b/tests/test_integration.py index 7ebcf6f..75fec69 100644 --- a/tests/test_integration.py +++ b/tests/test_integration.py @@ -11,6 +11,7 @@ from pathlib import Path import pytest from moongreet.greeter import faillock_warning, FAILLOCK_MAX_ATTEMPTS +from moongreet.i18n import load_strings from moongreet.ipc import create_session, post_auth_response, start_session, cancel_session @@ -182,22 +183,26 @@ class TestFaillockWarning: """Tests for the faillock warning message logic.""" def test_no_warning_on_first_attempt(self) -> None: - assert faillock_warning(1) is None + strings = load_strings("de") + assert faillock_warning(1, strings) is None def test_warning_on_second_attempt(self) -> None: - warning = faillock_warning(2) + strings = load_strings("de") + warning = faillock_warning(2, strings) assert warning is not None assert "1" in warning # 1 Versuch übrig def test_warning_on_third_attempt(self) -> None: - warning = faillock_warning(3) + strings = load_strings("de") + warning = faillock_warning(3, strings) assert warning is not None - assert "gesperrt" in warning.lower() + assert warning == strings.faillock_locked def test_warning_beyond_max_attempts(self) -> None: - warning = faillock_warning(4) + strings = load_strings("de") + warning = faillock_warning(4, strings) assert warning is not None - assert "gesperrt" in warning.lower() + assert warning == strings.faillock_locked def test_max_attempts_constant_is_three(self) -> None: assert FAILLOCK_MAX_ATTEMPTS == 3 diff --git a/tests/test_users.py b/tests/test_users.py index 0180ca8..f357f8c 100644 --- a/tests/test_users.py +++ b/tests/test_users.py @@ -187,7 +187,7 @@ class TestGetUserGtkTheme: assert result is None def test_handles_interpolation_characters(self, tmp_path: Path) -> None: - """Theme names with % characters should not trigger interpolation errors.""" + """Theme names with % characters are rejected by validation.""" gtk_dir = tmp_path / ".config" / "gtk-4.0" gtk_dir.mkdir(parents=True) settings = gtk_dir / "settings.ini" @@ -195,7 +195,18 @@ class TestGetUserGtkTheme: result = get_user_gtk_theme(config_dir=gtk_dir) - assert result == "My%Theme" + assert result is None + + def test_rejects_path_traversal_theme_name(self, tmp_path: Path) -> None: + """Theme names with path traversal characters should be rejected.""" + gtk_dir = tmp_path / ".config" / "gtk-4.0" + gtk_dir.mkdir(parents=True) + settings = gtk_dir / "settings.ini" + settings.write_text("[Settings]\ngtk-theme-name=../../../../etc/evil\n") + + result = get_user_gtk_theme(config_dir=gtk_dir) + + assert result is None def test_ignores_symlinked_accountsservice_icon(self, tmp_path: Path) -> None: """AccountsService icon as symlink should be ignored to prevent traversal."""