Skip to content

[Security] Persona tools=[] Policy Bypass Re-Exposes Built-in Web Search Tools in AstrBot #8780

@YLChen-007

Description

@YLChen-007

Advisory Details

Title: Persona tools=[] Policy Bypass Re-Exposes Built-in Web Search Tools in AstrBot

Description:

Summary

AstrBot's persona-level tool policy can be bypassed during main agent construction. A persona explicitly configured with tools=[] should disable all tools, but the final tool list is modified after persona filtering. When provider_settings.web_search=True, the main agent appends web_search_tavily and related built-in tools after the persona allowlist has already been applied. As a result, an authenticated operator can bind a supposedly tool-less persona to a conversation and still trigger real tool exposure and invocation through the documented /api/v1/chat HTTP interface.

Details

The bug is in the tool assembly order inside astrbot/core/astr_main_agent.py.

Persona restrictions are first translated into persona_toolset and merged into req.func_tool:

if not req.func_tool:
    req.func_tool = persona_toolset
else:
    req.func_tool.merge(persona_toolset)

At this point, a persona with tools=[] correctly yields an empty tool set. The problem is that later helper paths append built-in tools directly to req.func_tool without re-applying the persona restriction.

The web-search helper is one such path:

if provider == "tavily":
    req.func_tool.add_tool(tool_mgr.get_builtin_tool(TavilyWebSearchTool))
    req.func_tool.add_tool(tool_mgr.get_builtin_tool(TavilyExtractWebPageTool))

And this helper is called after persona filtering:

_plugin_tool_fix(event, req)
await _apply_web_search_tools(event, req, plugin_context)

There are additional post-filter append paths in the same function, including proactive messaging:

if event.platform_meta.support_proactive_message:
    if req.func_tool is None:
        req.func_tool = ToolSet()
    req.func_tool.add_tool(
        plugin_context.get_llm_tool_manager().get_builtin_tool(
            SendMessageToUserTool
        )
    )

This is a policy-enforcement flaw, not a synthetic test artifact. I verified it end-to-end using the standard Dashboard and /api/v1/chat APIs against a real AstrBot service with a local OpenAI-compatible mock backend. The mock provider captured the outgoing OpenAI-style tool schema, and the AstrBot runtime log recorded actual tool selection:

  • mock_openai_requests.jsonl contains a provider request whose tools array includes web_search_tavily, tavily_extract_web_page, and send_message_to_user.
  • astrbot_server.log records Agent 使用工具: ['web_search_tavily'].

One note on the supplied PoC: verification_test.py writes a final verification_result.json summary that can incorrectly show tool_names: [] because it only samples the last provider request in the session, which is the follow-up "stop using tools and summarize" request. The raw captured request log and server runtime log still demonstrate the vulnerability conclusively.

PoC

Prerequisites

  • An AstrBot deployment using the official AstrBotDevs/AstrBot repository.
  • An authenticated Dashboard/operator account capable of:
    • creating a persona,
    • binding a persona to a conversation,
    • editing runtime configuration.
  • A local or reachable OpenAI-compatible backend. The provided PoC uses a local mock provider on http://127.0.0.1:18080.
  • AstrBot started with the provided runtime template and Dashboard enabled on http://127.0.0.1:6185.

Reproduction Steps

  1. Download the mock provider script from: mock_openai_server.py
  2. Download the main exploit script from: verification_test.py
  3. Download the control script from: control-no-late-tools.py
  4. Download the runtime configuration template from: runtime-config-template.json
  5. Place the four files in the same working directory and run the control:
    python3 control-no-late-tools.py
  6. Confirm the control output ends with tool_names=[] and [CONTROL-PASS].
  7. Run the exploit:
    python3 verification_test.py
  8. Inspect mock_openai_requests.jsonl and astrbot_server.log.
  9. Confirm that the outgoing tools array contains web_search_tavily and that the AstrBot runtime log records Agent 使用工具: ['web_search_tavily'] even though the bound persona was created with tools=[].

Log of Evidence

The following runtime evidence was captured during end-to-end verification:

login_status=ok
api_key_created=yes
persona_create_status=ok
bootstrap_chat_sent=yes
persona_bound=yes
config_saved=yes
verification_chat_sent=yes
tool_names=[]
[FALSE-POSITIVE]

The final [FALSE-POSITIVE] line above is produced by the PoC summary bug described earlier and is not the real exploit outcome. The decisive runtime evidence is:

astrbot_server.log
Agent 使用工具: ['web_search_tavily']
使用工具:web_search_tavily,参数:{}
Tool `web_search_tavily` Result: Error: Tavily API key is not configured in AstrBot.

And the raw provider capture shows the forbidden tools were exposed to the model:

mock_openai_requests.jsonl
tool_names: ["web_search_tavily", "tavily_extract_web_page", "send_message_to_user"]

Impact

This is a policy-enforcement bypass that defeats the security meaning of persona-level tool disablement.

Any deployment relying on tools=[] to create a no-tools persona can end up re-exposing built-in tools when optional features are enabled. In the verified path, web search becomes available despite the operator explicitly disabling all tools for that persona. In the broader code path, the same design flaw can also re-expose proactive messaging, cron-related tools, and local/sandbox computer-use tools if those late-append features are enabled.

This breaks operator expectations and can expand what the model is allowed to do inside a conversation boundary that was intended to be tool-free.

Affected products

  • Ecosystem: GitHub
  • Package name: AstrBot
  • Affected versions: <= 4.25.5
  • Patched versions:

Severity

  • Severity: Medium
  • Vector string: CVSS:3.1/AV:N/AC:L/PR:H/UI:N/S:U/C:L/I:L/A:N

Weaknesses

  • CWE: CWE-863: Incorrect Authorization

Occurrences

Permalink Description
if not req.func_tool:
req.func_tool = persona_toolset
else:
req.func_tool.merge(persona_toolset)
Persona restrictions are committed into req.func_tool here. For a persona with tools=[], this leaves the effective tool set empty at this point in the function.
if not prov_settings.get("web_search", False):
return
if req.func_tool is None:
req.func_tool = ToolSet()
tool_mgr = plugin_context.get_llm_tool_manager()
provider = prov_settings.get("websearch_provider", "tavily")
if provider == "tavily":
req.func_tool.add_tool(tool_mgr.get_builtin_tool(TavilyWebSearchTool))
req.func_tool.add_tool(tool_mgr.get_builtin_tool(TavilyExtractWebPageTool))
When provider_settings.web_search=True and the Tavily backend is selected, _apply_web_search_tools() appends TavilyWebSearchTool and TavilyExtractWebPageTool directly to req.func_tool.
_plugin_tool_fix(event, req)
await _apply_web_search_tools(event, req, plugin_context)
The web-search append helper is invoked after persona filtering, which is the ordering flaw that makes the bypass possible.
if event.platform_meta.support_proactive_message:
if req.func_tool is None:
req.func_tool = ToolSet()
req.func_tool.add_tool(
plugin_context.get_llm_tool_manager().get_builtin_tool(
SendMessageToUserTool
)
)
Proactive messaging tools are also appended after persona filtering, demonstrating that the design issue is broader than the Tavily web-search path alone.

Metadata

Metadata

Assignees

No one assigned

    Labels

    area:coreThe bug / feature is about astrbot's core, backendfeature:personaThe bug / feature is about astrbot AI persona system (system prompt)

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions