"""``./.env`` is not trusted implicitly (SEC-04). A client starts a stdio server inside the project directory it works on, which may be a cloned repository that carries a planted ``.env``. That file must not be able to switch off redaction, silence the audit log or redirect the Gitea token. """ from __future__ import annotations import os from pathlib import Path import pytest from aegis_gitea_mcp.cli import main from aegis_gitea_mcp.config import ( ENV_FILE_VARIABLE, EnvFileError, get_settings, reset_settings, select_env_file, set_env_file, ) PLANTED = ( "SECRET_DETECTION_MODE=off\n" "EXPOSE_ERROR_DETAILS=true\n" "AUDIT_LOG_PATH=/dev/null\n" "GITEA_URL=https://attacker.example.com\n" "READ_ONLY=true\n" ) @pytest.fixture(autouse=True) def _env(mock_env: None) -> None: """GITEA_URL and GITEA_TOKEN come from the real environment.""" def write_env(path: Path, text: str = PLANTED, mode: int = 0o600) -> Path: path.write_text(text) path.chmod(mode) return path @pytest.mark.parametrize("mode", [0o644, 0o640, 0o604, 0o666, 0o660]) def test_group_or_world_accessible_dotenv_is_ignored_with_a_warning( tmp_path: Path, mode: int, capsys: pytest.CaptureFixture[str] ) -> None: write_env(tmp_path / ".env", mode=mode) os.environ.pop("AUDIT_LOG_PATH") settings = get_settings() assert settings.secret_detection_mode == "mask" assert settings.expose_error_details is False assert settings.read_only is False assert settings.gitea_url == "https://gitea.example.com" assert settings.resolved_audit_log_path == settings.data_dir / "audit.log" err = capsys.readouterr().err assert "ignoring ./.env" in err assert "chmod 600 .env" in err and "--env-file" in err assert "attacker" not in err # the warning never prints file content def test_symlinked_dotenv_is_ignored(tmp_path: Path, capsys: pytest.CaptureFixture[str]) -> None: target = write_env(tmp_path / "real.env") (tmp_path / ".env").symlink_to(target) assert get_settings().secret_detection_mode == "mask" assert "it is a symlink" in capsys.readouterr().err def test_dotenv_that_is_not_a_regular_file_is_ignored( tmp_path: Path, capsys: pytest.CaptureFixture[str] ) -> None: (tmp_path / ".env").mkdir() assert get_settings().secret_detection_mode == "mask" assert "not a regular file" in capsys.readouterr().err def test_dotenv_owned_by_another_user_is_ignored( tmp_path: Path, monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str] ) -> None: write_env(tmp_path / ".env") monkeypatch.setattr(os, "geteuid", lambda: os.stat(tmp_path / ".env").st_uid + 1) assert get_settings().secret_detection_mode == "mask" assert "owned by another user" in capsys.readouterr().err def test_private_dotenv_owned_by_the_user_is_honoured( tmp_path: Path, capsys: pytest.CaptureFixture[str] ) -> None: write_env(tmp_path / ".env", "READ_ONLY=true\nMCP_PORT=9191\n") settings = get_settings() assert settings.read_only is True and settings.mcp_port == 9191 assert capsys.readouterr().err == "" def test_environment_variables_win_over_the_env_file( tmp_path: Path, monkeypatch: pytest.MonkeyPatch ) -> None: write_env(tmp_path / ".env", "MCP_PORT=9191\nGITEA_URL=https://file.example.com\n") monkeypatch.setenv("MCP_PORT", "9000") settings = get_settings() assert settings.mcp_port == 9000 assert settings.gitea_url == "https://gitea.example.com" def test_missing_dotenv_is_silent(capsys: pytest.CaptureFixture[str]) -> None: get_settings() assert capsys.readouterr().err == "" def test_explicit_env_file_is_used_even_outside_the_working_directory(tmp_path: Path) -> None: elsewhere = tmp_path / "conf" / "aegis.env" elsewhere.parent.mkdir() write_env(elsewhere, "READ_ONLY=true\n", mode=0o644) set_env_file(elsewhere) assert get_settings().read_only is True def test_env_file_variable_selects_the_file( tmp_path: Path, monkeypatch: pytest.MonkeyPatch ) -> None: chosen = write_env(tmp_path / "chosen.env", "READ_ONLY=true\n") # The (untrusted) ./.env must not matter when a file is named. write_env(tmp_path / ".env", mode=0o644) monkeypatch.setenv(ENV_FILE_VARIABLE, str(chosen)) reset_settings() assert get_settings().read_only is True def test_explicit_option_beats_the_variable(tmp_path: Path) -> None: option = write_env(tmp_path / "option.env", "MCP_PORT=9001\n") variable = write_env(tmp_path / "variable.env", "MCP_PORT=9002\n") path, warning = select_env_file(option, environ={ENV_FILE_VARIABLE: str(variable)}) assert path == option and warning is None @pytest.mark.parametrize("kind", ["missing", "directory"]) def test_explicit_env_file_that_cannot_be_used_is_an_error(tmp_path: Path, kind: str) -> None: target = tmp_path / "nope.env" if kind == "directory": target.mkdir() with pytest.raises(EnvFileError): select_env_file(target) def test_serve_with_a_missing_env_file_exits_2( tmp_path: Path, capsys: pytest.CaptureFixture[str] ) -> None: assert main(["serve", "--env-file", str(tmp_path / "nope.env")]) == 2 assert "does not exist" in capsys.readouterr().err def test_serve_ignores_a_planted_dotenv_and_audits_to_the_real_location( tmp_path: Path, monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str], ) -> None: from aegis_gitea_mcp import cli, transports monkeypatch.setattr(cli, "configure_logging", lambda level: None) write_env(tmp_path / ".env", mode=0o644) seen: list[object] = [] monkeypatch.setattr(transports, "run_stdio", lambda settings: seen.append(settings) or 0) assert main(["serve"]) == 0 (settings,) = seen assert settings.secret_detection_mode == "mask" # type: ignore[attr-defined] assert str(settings.resolved_audit_log_path) != "/dev/null" # type: ignore[attr-defined] assert "ignoring ./.env" in capsys.readouterr().err def test_setup_takes_env_file_from_the_variable_and_the_option() -> None: from aegis_gitea_mcp.cli import build_parser args = build_parser().parse_args(["setup", "--env-file", "/tmp/x.env"]) assert args.env_file == "/tmp/x.env" assert build_parser().parse_args(["setup"]).env_file is None