Skip to content

[Bug]: QQBot credential backups bypass gateway state isolation #84313

Description

@coygeek

Severity Assessment

CVSS Assessment

Metric v3.1 v4.0
Score 8.4 / 10.0 9.2 / 10.0
Severity High Critical
Vector CVSS:3.1/AV:L/AC:L/PR:L/UI:N/S:C/C:H/I:H/A:N CVSS:4.0/AV:L/AC:L/AT:N/PR:L/UI:N/VC:H/VI:H/VA:N/SC:H/SI:H/SA:N
Calculator CVSS v3.1 Calculator CVSS v4.0 Calculator

Threat Model Alignment

Classification: security-specific

openclaw/SECURITY.md says "For advanced setups, multiple gateways on one machine are possible, but only with strict isolation" and "For untrusted-user isolation, split by trust boundary: separate gateways and separate OS users/hosts per boundary." This finding breaks the documented separate-gateway isolation layer because QQBot persists credentials in a home-global path instead of the active OPENCLAW_STATE_DIR. It is not the out-of-scope shared-gateway case because the repro uses two different gateway instances with different configs and state directories, matching docs/gateway/index.md:157-185 and docs/gateway/multiple-gateways.md:119-127.

Impact

A QQBot account that reaches READY on Gateway A writes a reusable credential backup into ~/.openclaw/qqbot/data/. Gateway B, started under the same OS account but a different OPENCLAW_STATE_DIR, can silently import that backup and overwrite its own config with Gateway A's appId and clientSecret, collapsing two supposedly isolated gateway profiles into one shared QQBot identity.

Affected Component

File: extensions/qqbot/src/engine/utils/platform.ts:42-45, extensions/qqbot/src/engine/utils/data-paths.ts:31-37, extensions/qqbot/src/channel.ts:168-170,282-295

export function getQQBotDataPath(...subPaths: string[]): string {
  return path.join(getHomeDir(), ".openclaw", "qqbot", ...subPaths);
}

export function getCredentialBackupFile(accountId: string): string {
  return path.join(getQQBotDataPath("data"), `credential-backup-${safeName(accountId)}.json`);
}

if (account.appId && account.clientSecret) {
  saveCredentialBackup(account.accountId, account.appId, account.clientSecret);
}

if (!account.appId || !account.clientSecret) {
  const backup = loadCredentialBackup(account.accountId);
  if (backup?.appId && backup?.clientSecret) {
    const nextCfg = applyQQBotAccountConfig(cfg, account.accountId, {
      appId: backup.appId,
      clientSecret: backup.clientSecret,
    });
    await writeOpenClawConfigThroughRuntime(getQQBotRuntime(), nextCfg);
  }
}

Technical Reproduction

  1. Start Gateway A and Gateway B under the same $HOME but with different OPENCLAW_CONFIG_PATH and OPENCLAW_STATE_DIR values, following the multi-gateway isolation guidance in docs/gateway/index.md:157-185.
  2. Configure QQBot on Gateway A with account id default (or any shared account id), let the channel reach READY, and observe persistAccountCredentialSnapshot() call saveCredentialBackup().
  3. Because getCredentialBackupFile() resolves through getQQBotDataPath(), Gateway A writes credential-backup-default.json under ~/.openclaw/qqbot/data/ instead of under Gateway A's state directory.
  4. Start Gateway B with the same QQBot account id but missing appId or clientSecret. During qqbotPlugin.gateway.startAccount(), loadCredentialBackup(account.accountId) reads Gateway A's shared backup and writeOpenClawConfigThroughRuntime() injects those credentials into Gateway B's config.
  5. Observe that Gateway B now connects with Gateway A's QQBot credentials even though both gateways use different state directories and were expected to stay isolated.

Demonstrated Impact

The exploit path is deterministic and does not require modifying trusted local files ahead of time: ordinary QQBot startup on Gateway A creates the credential backup, and ordinary QQBot startup on Gateway B consumes it. The root cause is architectural coupling to os.homedir()/~/.openclaw/qqbot rather than the active gateway state resolver. Existing controls do not prevent the cross-profile import because the restore path runs before the channel starts, explicitly rewrites Gateway B's config with the recovered secret, and keys the backup only by accountId. That lets one isolated gateway profile inherit another profile's bot identity and message authority, violating the documented per-instance config/state split.

Environment

Inspected in openclaw/ at upstream/main commit 3d96111a5afe377b529c9bb5a9db510d74607344. The vulnerable storage and restore paths are implemented in extensions/qqbot/src/engine/utils/platform.ts, extensions/qqbot/src/engine/utils/data-paths.ts, and extensions/qqbot/src/channel.ts. The expected isolation model is documented in docs/gateway/index.md:157-185, docs/gateway/multiple-gateways.md:119-127, and openclaw/SECURITY.md:136-139,236.

Remediation Advice

Route all QQBot durable state, especially credential backups, through the active OpenClaw state-directory resolver and include the gateway profile/state directory in the backup scope so one gateway instance cannot restore another instance's QQBot credentials.

Metadata

Metadata

Assignees

Labels

P0Emergency: data loss, security bypass, crash loop, or unusable core runtime.clawsweeper:linked-pr-openClawSweeper found an open linked pull request for this issue.clawsweeper:needs-security-reviewClawSweeper marked this issue as needing security-sensitive review.clawsweeper:no-new-fix-prClawSweeper does not recommend queueing a new automated fix PR for this issue.clawsweeper:source-reproClawSweeper found a high-confidence source-level issue reproduction.impact:auth-providerAuth, provider routing, model choice, or SecretRef resolution may break.impact:securitySecurity boundary, credential, authz, sandbox, or sensitive-data risk.issue-rating: 🦞 diamond lobsterVery strong issue quality with high-confidence source-level or clear reproduction.

Type

No type

Fields

Priority

None yet

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions