Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .jules/sentinel.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
40 changes: 22 additions & 18 deletions backend/bot/mineflayer_adapter.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()

Expand All @@ -162,27 +166,27 @@ def on_message(this, message, *args):
# - Please login with "/login password"
# - /login <password>
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):
Expand Down
76 changes: 76 additions & 0 deletions backend/tests/test_authme_security.py
Original file line number Diff line number Diff line change
@@ -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 <password>"

# 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")