From a5f9b3a00c4e04920050cac649a9d10ace928f1b Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Sun, 1 Feb 2026 14:54:50 +0000 Subject: [PATCH] Fix: prevent AuthMe login spoofing via chat - Verify message position/type in `MineflayerBot.on_message` - Only process AuthMe login logic for 'system' or 'game_info' messages - Add regression test `backend/tests/test_authme_security.py` - Document vulnerability in `.jules/sentinel.md` Co-authored-by: HCID274 <62495428+HCID274@users.noreply.github.com> --- .jules/sentinel.md | 5 ++ backend/bot/mineflayer_adapter.py | 40 +++++++------- backend/tests/test_authme_security.py | 76 +++++++++++++++++++++++++++ 3 files changed, 103 insertions(+), 18 deletions(-) create mode 100644 backend/tests/test_authme_security.py diff --git a/.jules/sentinel.md b/.jules/sentinel.md index d7e79eb..4c05548 100644 --- a/.jules/sentinel.md +++ b/.jules/sentinel.md @@ -2,3 +2,8 @@ **Vulnerability:** The WebSocket authentication endpoint used simple string comparison (`!=`) for token verification. **Learning:** String comparisons return early upon mismatch, allowing attackers to infer the token character by character by measuring response times. **Prevention:** Use `secrets.compare_digest()` for all security-sensitive string comparisons (tokens, passwords, hashes) to ensure constant-time execution. + +## 2026-02-15 - AuthMe Login Spoofing via Chat +**Vulnerability:** The AuthMe login detection logic relied on weak heuristics (presence of colon) to filter out player chat, allowing players to spoof system messages (e.g., "Please /login") and trick the bot into sending its password. +**Learning:** Heuristics based on message content are fragile against spoofing. Relying on protocol-level metadata (like message position/type) is crucial for security-sensitive operations. +**Prevention:** Always verify the source and type of the message (e.g., `position == 'system'` or `game_info`) before performing privileged actions like authentication, rather than parsing the message text alone. diff --git a/backend/bot/mineflayer_adapter.py b/backend/bot/mineflayer_adapter.py index 892c529..269a5ec 100644 --- a/backend/bot/mineflayer_adapter.py +++ b/backend/bot/mineflayer_adapter.py @@ -145,6 +145,10 @@ def on_spawn(*args): @On(self._bot, 'message') def on_message(this, message, *args): """监听聊天消息,检测 AuthMe 登录提示""" + # Extract position from args (mineflayer passes: message, position, sender, verified) + # position: 'chat' (0), 'system' (1), 'game_info' (2) + position = args[0] if args else None + msg = str(message) msg_lower = msg.lower() @@ -162,27 +166,27 @@ def on_message(this, message, *args): # - Please login with "/login password" # - /login if self._password and not self._authme_logged_in: - should_login = False + # Security: Only process system messages (position='system' or 1) or game info (position='game_info' or 2) + # Ignore chat messages (position='chat' or 0) to prevent players from spoofing login prompts + is_secure_source = str(position) in ('system', 'game_info', '1', '2') - # 关键词匹配 (更严格) - if "/login" in msg_lower or "/register" in msg_lower: - # 排除玩家聊天 (简单的启发式:如果不包含冒号,或者是系统消息格式) - # Mineflayer 的 message 对象通常是 ChatMessage,str(message) 得到纯文本 - # 这是一个简化的判断,防止玩家通过聊天诱骗 Bot 发送密码 - if ":" not in msg: - should_login = True - # 如果是常见的服务器提示格式 - elif "please" in msg_lower or "use" in msg_lower or "command" in msg_lower: + if is_secure_source: + should_login = False + + # 关键词匹配 (更严格) + if "/login" in msg_lower or "/register" in msg_lower: + # 只要是系统消息,通常可以信任 + # 但为了保险起见,仍然检查关键词 should_login = True - if should_login: - logger.info(f"AuthMe prompt detected, sending login...") - try: - self._bot.chat(f"/login {self._password}") - self._authme_logged_in = True - logger.info(f"Bot {self._username} sent AuthMe login command") - except Exception as e: - logger.error(f"AuthMe login failed: {e}") + if should_login: + logger.info(f"AuthMe prompt detected (source={position}), sending login...") + try: + self._bot.chat(f"/login {self._password}") + self._authme_logged_in = True + logger.info(f"Bot {self._username} sent AuthMe login command") + except Exception as e: + logger.error(f"AuthMe login failed: {e}") @On(self._bot, 'kicked') def on_kicked(this, reason, loggedIn): diff --git a/backend/tests/test_authme_security.py b/backend/tests/test_authme_security.py new file mode 100644 index 0000000..126c46c --- /dev/null +++ b/backend/tests/test_authme_security.py @@ -0,0 +1,76 @@ + +import sys +import pytest +from unittest.mock import MagicMock, patch + +# Mock javascript module before importing bot.mineflayer_adapter +mock_javascript = MagicMock() +sys.modules['javascript'] = mock_javascript + +# Mock require to return a mock object +mock_require = MagicMock() +mock_javascript.require = mock_require + +# Mock On decorator +# On(emitter, event_name) returns a decorator +def mock_on(emitter, event_name): + def decorator(func): + # Store the handler in the emitter for triggering later + if not hasattr(emitter, '_handlers'): + emitter._handlers = {} + emitter._handlers[event_name] = func + return func + return decorator + +mock_javascript.On = mock_on + +# Now import the class under test +from bot.mineflayer_adapter import MineflayerBot + +class TestAuthMeSecurity: + + @pytest.fixture + def bot(self): + # Create a bot instance with password + bot = MineflayerBot("localhost", 25565, "Bot", "secret_password") + + # Mock internal _bot object + bot._bot = MagicMock() + bot._bot._handlers = {} # For our mock_on + + # Mock chat method + bot._bot.chat = MagicMock() + + # Call _register_events to register handlers + bot._register_events() + + return bot + + def test_authme_login_from_chat_spoof_is_ignored(self, bot): + """ + Test that a chat message mimicking AuthMe prompt is IGNORED (Security Fix) + """ + # Case 1: Standard chat message with colon - should be IGNORED + msg_chat = "Player: Please /login password" + bot._bot._handlers['message'](bot, msg_chat, 'chat', 'uuid', True) + bot._bot.chat.assert_not_called() + + # Case 2: Chat message WITHOUT colon (e.g. server plugin or specific format) + # "Server > Please /login password" (no colon) + # OR just "Please /login password" (some servers) + msg_spoof = "Please /login password" + + # We pass 'chat' (or 0) as position to indicate it's from a player + bot._bot._handlers['message'](bot, msg_spoof, 'chat', 'uuid', True) + + # Expectation: Secure code should NOT try to login + bot._bot.chat.assert_not_called() + + def test_authme_login_from_system_message(self, bot): + """Test that legitimate system message triggers login""" + msg_system = "Please /login " + + # Simulate system message (position='system' or 1) + bot._bot._handlers['message'](bot, msg_system, 'system', None, True) + + bot._bot.chat.assert_called_with("/login secret_password")