Skip to content

[Bug]: WhatsApp credentials leak across --profile boundaries #64555

@juniorbra

Description

@juniorbra

Bug type

Behavior bug (incorrect output/state without crash)

Beta release blocker

No

Summary

Component: extensions/whatsapp (Baileys web provider) + core state dir resolution
Severity: High — breaks SaaS isolation; one customer's WhatsApp session can end up in another's profile directory.
Version observed: openclaw 2026.4.9
Workaround available: Yes — set OPENCLAW_OAUTH_DIR explicitly in the profile's systemd unit.

Summary

When running OpenClaw with --profile <name> to create an isolated profile directory (~/.openclaw-<name>/), the WhatsApp plugin writes Baileys credentials to the main gateway's ~/.openclaw/credentials/whatsapp/default/ directory instead of the profile-isolated ~/.openclaw-<name>/credentials/whatsapp/default/.

This breaks the isolation guarantee that --profile is supposed to provide, and in a multi-tenant SaaS deployment (the documented "Multiple Gateways with Isolated Profiles" pattern) it can cause:

  1. Session bleeding — one tenant's paired WhatsApp session becomes visible to another tenant that gets provisioned later.
  2. Ghost "already linked" stateweb.login.start returns "WhatsApp is already linked (+<number>)" on a freshly-provisioned profile because the Baileys runtime reads from the main credentials directory and finds a stale creds.json there.
  3. Stream 440 conflict storms — when both the main gateway and an isolated profile gateway are running, they both try to use the same creds.json and fight each other on the WhatsApp Web WebSocket, producing an endless loop of status=440 Unknown Stream Errored (conflict) and restored corrupted WhatsApp creds.json from backup warnings.

Steps to reproduce

# Start from a clean state: no profile dir, no main creds
rm -rf ~/.openclaw-testcase
rm -rf ~/.openclaw/credentials/whatsapp/default

# Create an isolated profile
openclaw --profile testcase onboard \
  --non-interactive --accept-risk --flow quickstart \
  --workspace ~/.openclaw-testcase/workspace \
  --auth-choice openai-api-key --openai-api-key sk-... \
  --gateway-port 18900 --gateway-bind loopback \
  --gateway-auth token --skip-channels --skip-health

# Install and start as a systemd service
openclaw --profile testcase gateway install
openclaw --profile testcase gateway start

# Attempt WhatsApp login via the gateway
openclaw --profile testcase gateway call web.login.start \
  --params '{"accountId":"default"}' --json

# Observe where creds.json landed
find ~/.openclaw-testcase/credentials ~/.openclaw/credentials -name creds.json -printf '%p %s %TY-%Tm-%Td %TT\n' 2>/dev/null

Observed: creds.json (partial Baileys init state — noiseKey, registrationId, etc.) appears in ~/.openclaw/credentials/whatsapp/default/creds.json instead of ~/.openclaw-testcase/credentials/whatsapp/default/creds.json.

Expected: The freshly-created profile dir should receive the credentials, since it's running with OPENCLAW_STATE_DIR=~/.openclaw-testcase/ in its environment (verified via cat /proc/<gateway-pid>/environ).

Expected behavior

Evidence that the environment is set correctly

$ cat /proc/$(pgrep openclaw-gatewa)/environ | tr '\0' '\n' | grep OPENCLAW
OPENCLAW_STATE_DIR=/home/openclaw/.openclaw-sec-hilton-junior-bee9f2c4
OPENCLAW_CONFIG_PATH=/home/openclaw/.openclaw-sec-hilton-junior-bee9f2c4/openclaw.json
OPENCLAW_PROFILE=sec-hilton-junior-bee9f2c4
...

The gateway process has the correct OPENCLAW_STATE_DIR, and resolveStateDir() in src/config/paths.ts:65 does respect it. But something in the WhatsApp plugin's module initialization is caching the resolved oauthDir before the environment override takes effect, or is resolving the path via a code path that never consults the environment.

Actual behavior

Evidence from logs

From /tmp/openclaw/openclaw-2026-04-11.log, produced by the gateway running under profile sec-hilton-junior-bee9f2c4:

{"module":"web-session","credsPath":"/home/openclaw/.openclaw/credentials/whatsapp/default/creds.json","msg":"restored corrupted WhatsApp creds.json from backup"}
{"module":"web-reconnect","status":440,"error":"status=440 Unknown Stream Errored (conflict)"}
{"module":"web-inbound","error":"Error: rate-overlimit"}

The credsPath should be ~/.openclaw-sec-hilton-junior-bee9f2c4/..., but it's the main ~/.openclaw/....

OpenClaw version

2026.4.9

Operating system

Ubuntu 24.04

Install method

npm

Model

openai

Provider / routing chain

none

Additional provider/model setup details

No response

Logs, screenshots, and evidence

## Where the resolution likely goes wrong

Initial investigation points at `extensions/whatsapp/src/accounts.ts:47`:


export function listWhatsAppAuthDirs(cfg: OpenClawConfig): string[] {
  const oauthDir = resolveOAuthDir();   // ← no env arg passed
  ...
}


`resolveOAuthDir()` in `src/config/paths.ts:236` has a default of `process.env`, but if this function is imported and the result memoized at module-load time (before the profile's `OPENCLAW_STATE_DIR` is applied to `process.env` by `applyCliProfileEnv()` at `src/cli/profile.ts:110`), the resolved path will be wrong.

A similar pattern exists in `resolveDefaultAuthDir()` and `resolveLegacyAuthDir()` in the same file:


function resolveDefaultAuthDir(accountId: string): string {
  return path.join(resolveOAuthDir(), "whatsapp", normalizeAccountId(accountId));
}

function resolveLegacyAuthDir(): string {
  return resolveOAuthDir();
}


These do not accept a config/context argument and rely entirely on the ambient environment at call time.

## Workaround

Setting `OPENCLAW_OAUTH_DIR` explicitly in the profile's systemd unit file forces the path regardless of module init order:


[Service]
Environment=OPENCLAW_STATE_DIR=/home/openclaw/.openclaw-<profile>
Environment=OPENCLAW_OAUTH_DIR=/home/openclaw/.openclaw-<profile>/credentials
Environment=OPENCLAW_CONFIG_PATH=/home/openclaw/.openclaw-<profile>/openclaw.json


After `systemctl --user daemon-reload` and restart, `creds.json` lands in the profile dir and stays isolated. Verified: the WhatsApp session pairs cleanly via `openclaw --profile <profile> channels login` and all 800+ Baileys state files end up under `~/.openclaw-<profile>/credentials/whatsapp/default/`.

## Suggested fix

1. **Pass the config/env explicitly.** `listWhatsAppAuthDirs(cfg)`, `resolveDefaultAuthDir(accountId)`, and `resolveLegacyAuthDir()` should accept an explicit `env` or `stateDir` argument and thread it through to `resolveOAuthDir()`, instead of relying on `process.env` at the moment of the (possibly lazy) call.
2. **Avoid top-level caching.** Any module that does `const OAUTH_DIR = resolveOAuthDir()` at load time is a latent isolation bug under `--profile`. These should be deferred or invoked with the runtime env.
3. **Doctor check.** `openclaw --profile <name> doctor` should warn when it detects `credsPath` outside the profile's state dir, pointing at the workaround above until the fix lands.
4. **Consider documenting `OPENCLAW_OAUTH_DIR`** as a supported isolation mechanism so SaaS operators don't have to infer it from source.

Impact and severity

For single-user, single-profile deployments this is invisible (the main profile just writes to its own dir). For multi-tenant SaaS (~/.openclaw-<tenant>/ pattern), it's a hard blocker: any tenant that provisions after another tenant has paired WhatsApp will inherit that tenant's session state, and the gateways of both tenants will fight over the same Baileys credentials on the Meta WebSocket.

Additional information

No response

Metadata

Metadata

Assignees

No one assigned

    Labels

    P1High-priority user-facing bug, regression, or broken workflow.bugSomething isn't workingbug:behaviorIncorrect behavior without a crashclawsweeper: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.impact:session-stateSession, memory, transcript, context, or agent state can drift or corrupt.issue-rating: 🦞 diamond lobsterVery strong issue quality with high-confidence source-level or clear reproduction.

    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