Back to skill

Security audit

Codex Account Switcher

Security checks for vulnerabilities and agentic risk

Overview

This skill is coherent account-management tooling, but it handles and persists OAuth credentials in ways users should review before installing.

Install only if you are comfortable with a skill reading OpenClaw and Codex OAuth auth files, sending access tokens to ChatGPT's usage endpoint, changing OpenClaw account order, and creating local credential backups. Before running the sync command, verify file permissions on the OpenClaw auth directory, remove stale .bak credential files when no longer needed, and use dry-run modes first.

Vulnerability Patterns
  • Insecure Skill Coding PracticesFinds exploitable flaws such as hardcoded secrets or command injection
  • Skill Instruction HijackingAlters the agent's session goals or safety constraints when the skill loads
  • Agent Memory PoisoningWrites attacker-controlled rules into memory that affect later sessions
  • Remote Payload Retrieval and ExecutionFetches external code whose behavior can change after review
  • Embedded Malicious CodeShips malicious scripts inside the skill and executes them locally
Findings (1)

T09 · Insecure Skill Coding Practices

Warning
Location
scripts/codex-cli-sync.py:55
Finding
Plaintext OAuth credential backups may be created with unsafe permissions## Vulnerability Details **File Location**: `scripts/codex-cli-sync.py`, lines 55–59 **Vulnerability Type**: Insecure storage of sensitive credentials **Risk Level**: Medium ### Vulnerable Code ```python def backup_file(path: Path) -> Path | None: if not path.exists(): return None backup = path.with_name(f"{path.name}.bak-{time.strftime('%Y%m%d-%H%M%S')}") backup.write_bytes(path.read_bytes()) return backup ``` The function is called before the token-bearing OpenClaw profile store is updated: ```python prof_backup = backup_file(PROFILES_PATH) state_backup = backup_file(STATE_PATH) if not args.no_set_first else None store["profiles"][profile_id] = credential atomic_write_json(PROFILES_PATH, store) ``` ### Technical Analysis `auth-profiles.json` contains OAuth access and refresh tokens. Before modifying that file, `backup_file()` creates a complete plaintext copy under a timestamped `.bak-*` filename. The backup is created through `Path.write_bytes()` without explicitly setting owner-only permissions. Its effective permissions therefore depend on the process umask and parent-directory access controls. Under a permissive environment, another local user or process may be able to read the backup. These backups also have no retention or cleanup mechanism. Consequently, expired and potentially still-valid refresh tokens can remain on disk after the active credential store has changed. This increases both the number of credential copies and the duration of exposure. ### Attack Path 1. A legitimate user runs `python3 scripts/codex-cli-sync.py`. 2. The script reads the existing OpenClaw profile store containing OAuth credentials. 3. `backup_file()` writes the entire store to a timestamped plaintext backup. 4. The backup receives permissions based on the runtime umask rather than an explicit `0600` policy. 5. If the file and parent directories are accessible, another local principa ...[truncated 919 chars]
Remediation
## Remediation Suggestions - Create backup files atomically with explicit owner-only permissions, such as `os.open()` with `O_CREAT | O_EXCL | O_WRONLY` and mode `0o600`. - Apply `os.chmod(backup, 0o600)` after creation as defense in depth, while avoiding any interval in which the file is broadly readable. - Verify that the OpenClaw credential directory is owner-controlled and not writable or traversable by unrelated users. - Preserve restrictive permissions when replacing the primary credential store; explicitly enforce `0600` on token-bearing files. - Implement bounded backup retention and promptly remove obsolete credential backups. - Warn users that backups contain complete plaintext credentials. - Revoke affected OAuth sessions and delete exposed backups if permissive backups may already have been created. - Consider encrypting backups with a user-controlled key or omitting automatic credential backups unless the user explicitly requests them.
Vulnerability Patterns
  • Data ExfiltrationExternal Transmission, Env Variable Harvesting, File System Enumeration
  • Privilege EscalationExcessive Permissions, Sudo/Root Execution, Credential Access
  • Behavioral ASTexec() Call, eval() Call, Dynamic Import
  • Taint TrackingDirect Taint Flow, Variable-Mediated Taint Flow, Credential Exfiltration Chain
  • MCP Least PrivilegeUnderdeclared Capability, Wildcard Permission, Missing Permission Declaration
Findings (24)

Tainted flow: 'req' from os.environ.get (line 177, credential/environment) → urllib.request.urlopen (network output)

Critical
Category
Data Flow
Content
headers["ChatGPT-Account-Id"] = str(cred["accountId"])
    req = urllib.request.Request(WHAM_URL, headers=headers)
    try:
        with urllib.request.urlopen(req, timeout=8) as res:
            data = json.load(res)
    except Exception as e:
        return {"profileId": profile_id, "ok": False, "error": type(e).__name__}
Confidence
90% confidence
Finding
Credentials or environment variables flow to a network sink. This is a high-confidence indicator of credential exfiltration.

Tainted flow: 'req' from os.environ.get (line 177, credential/environment) → urllib.request.urlopen (network output)

Critical
Category
Data Flow
Content
headers["Authorization"] = "Bearer " + token
    req = urllib.request.Request(send_url, data=body, headers=headers, method="POST")
    try:
        with urllib.request.urlopen(req, timeout=8) as res:
            res.read(200)
    except Exception as exc:
        log(f"notify failed: {type(exc).__name__}: {exc}")
Confidence
95% confidence
Finding
The notification destination URL is loaded from configuration and can point to any scheme/host, then the script sends outbound requests to it. If an attacker can modify the config, this creates an SSRF/exfiltration primitive: notification contents and the NapCat bearer token may be sent to an attacker-controlled endpoint, and the script may be induced to contact internal services.

Tp4

High
Category
MCP Tool Poisoning
Confidence
99% confidence
Finding
A read/query/report-only execution path that is marketed as performing switching and failover is a material transparency problem, especially if it also exposes local status details not mentioned in the description. Misrepresentation of auth-related tooling reduces informed consent and can mask sensitive data access patterns from users and reviewers.

Tp4

High
Category
MCP Tool Poisoning
Confidence
97% confidence
Finding
A read/query/report-only execution path that is marketed as performing switching and failover is a material transparency problem, especially if it also exposes local status details not mentioned in the description. Misrepresentation of auth-related tooling reduces informed consent and can mask sensitive data access patterns from users and reviewers.

Tp4

High
Category
MCP Tool Poisoning
Confidence
98% confidence
Finding
A read/query/report-only execution path that is marketed as performing switching and failover is a material transparency problem, especially if it also exposes local status details not mentioned in the description. Misrepresentation of auth-related tooling reduces informed consent and can mask sensitive data access patterns from users and reviewers.

Tp4

High
Category
MCP Tool Poisoning
Confidence
97% confidence
Finding
A read/query/report-only execution path that is marketed as performing switching and failover is a material transparency problem, especially if it also exposes local status details not mentioned in the description. Misrepresentation of auth-related tooling reduces informed consent and can mask sensitive data access patterns from users and reviewers.

Credential Access

High
Category
Privilege Escalation
Content
## Behavior

- Reads OpenClaw auth profile metadata from the selected agent directory.
- Queries quota directly from ChatGPT WHAM usage using each profile's OAuth access token.
- Never prints access tokens, refresh tokens, API keys, or credential file contents.
- Account switching only rewrites `auth-state.json` provider order for `openai-codex`.
- `codex-cli-sync.py` is advanced/explicit: it imports the current Codex CLI `~/.codex/auth.json` login into OpenClaw and writes backups first.
Confidence
95% confidence
Finding
The skill states that it queries quota using each profile's OAuth access token, which confirms direct handling of bearer credentials for network access. Even if tokens are not printed, processing them and sending authenticated requests materially increases the risk of credential misuse, accidental leakage, or abuse if the scripts are modified, compromised, or insufficiently isolated.

Credential Access

High
Category
Privilege Escalation
Content
- Reads OpenClaw auth profile metadata from the selected agent directory.
- Queries quota directly from ChatGPT WHAM usage using each profile's OAuth access token.
- Never prints access tokens, refresh tokens, API keys, or credential file contents.
- Account switching only rewrites `auth-state.json` provider order for `openai-codex`.
- `codex-cli-sync.py` is advanced/explicit: it imports the current Codex CLI `~/.codex/auth.json` login into OpenClaw and writes backups first.
- Auto-switch defaults to switching only when the active account's 5h remaining quota is below `20%`.
Confidence
90% confidence
Finding
The claim that the skill never prints access tokens, refresh tokens, or credential file contents is helpful but also confirms the scripts operate on highly sensitive secrets and may import from `~/.codex/auth.json`. In this context, access to token-bearing files and auth state is inherently dangerous because compromise, unexpected logging, backups, or path confusion could expose reusable credentials.

Credential Access

High
Category
Privilege Escalation
Content
"""Fast local account/status query for OpenClaw NapCat commands.

Reads local auth profile metadata and prints a short Chinese summary. It never
prints access tokens, refresh tokens, API keys, or credential file contents.
"""
import json
import os
Confidence
77% confidence
Finding
The script handles credential material from local auth profiles and uses access tokens to query the remote usage endpoint, so it does access sensitive authentication data even if it does not print it. In an account-switching/quota-management skill, this raises the sensitivity of any other flaw in the file because arbitrary code execution or path hijacking could expose those tokens and profile metadata.

Lp3

Medium
Category
MCP Least Privilege
Confidence
92% confidence
Finding
The skill advertises and invokes scripts that read environment variables, access local credential files, write authentication state, and make network requests, but it declares no explicit tool scope or permission boundaries. In a credential-management skill, missing scope declarations increases the risk of overbroad execution and makes it harder for users or runners to understand and constrain sensitive operations.

Description-Behavior Mismatch

Medium
Confidence
96% confidence
Finding
The script explicitly reads Codex OAuth access and refresh tokens from ~/.codex/auth.json and imports them into OpenClaw credential storage, which expands the trust boundary and persists sensitive credentials in another location. This is more dangerous in this skill context because the manifest focuses on quota querying/account switching, so hidden credential replication creates unexpected credential exposure and increases the blast radius if OpenClaw storage is compromised.

External Transmission

Medium
Category
Data Exfiltration
Content
claims = {}
    for key in ("id_token", "access"):
        claims.update({k: v for k, v in decode_jwt_payload(tokens.get(key)).items() if k not in claims})
    email = normalize_email(claims.get("email") or claims.get("preferred_username") or claims.get("https://api.openai.com/profile/email"))
    user_id = str(claims.get("sub") or claims.get("user_id") or claims.get("https://api.openai.com/profile/user_id") or "").strip()
    account_id = str(tokens.get("account_id") or claims.get("https://api.openai.com/profile/account_id") or "").strip()
    expires_candidates = []
Confidence
60% confidence
Finding
Data is being sent to an external URL. This could be legitimate telemetry or data exfiltration. Manual review is recommended.

External Transmission

Medium
Category
Data Exfiltration
Content
claims = {}
    for key in ("id_token", "access"):
        claims.update({k: v for k, v in decode_jwt_payload(tokens.get(key)).items() if k not in claims})
    email = normalize_email(claims.get("email") or claims.get("preferred_username") or claims.get("https://api.openai.com/profile/email"))
    user_id = str(claims.get("sub") or claims.get("user_id") or claims.get("https://api.openai.com/profile/user_id") or "").strip()
    account_id = str(tokens.get("account_id") or claims.get("https://api.openai.com/profile/account_id") or "").strip()
    expires_candidates = []
Confidence
60% confidence
Finding
Data is being sent to an external URL. This could be legitimate telemetry or data exfiltration. Manual review is recommended.

External Transmission

Medium
Category
Data Exfiltration
Content
claims = {}
    for key in ("id_token", "access"):
        claims.update({k: v for k, v in decode_jwt_payload(tokens.get(key)).items() if k not in claims})
    email = normalize_email(claims.get("email") or claims.get("preferred_username") or claims.get("https://api.openai.com/profile/email"))
    user_id = str(claims.get("sub") or claims.get("user_id") or claims.get("https://api.openai.com/profile/user_id") or "").strip()
    account_id = str(tokens.get("account_id") or claims.get("https://api.openai.com/profile/account_id") or "").strip()
    expires_candidates = []
Confidence
60% confidence
Finding
Data is being sent to an external URL. This could be legitimate telemetry or data exfiltration. Manual review is recommended.

Description-Behavior Mismatch

Medium
Confidence
92% confidence
Finding
The code writes directly to OpenClaw auth-profiles.json and auth-state.json, changing both credential contents and account selection order rather than merely switching among preexisting accounts. In the context of an account-switching skill, modifying auth state is expected to some extent, but doing so by creating or overwriting auth profiles with imported tokens can silently alter authentication behavior and persist sensitive state changes.

subprocess module call

Medium
Category
Dangerous Code Execution
Content
if dry_run:
        return new_order
    cmd = ["openclaw", "models", "auth", "order", "set", "--provider", PROVIDER, *new_order]
    subprocess.run(cmd, check=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True)
    return new_order
Confidence
70% confidence
Finding
subprocess module calls execute external commands. Without careful input validation, this enables command injection.

Natural-Language Policy Violations

Medium
Confidence
95% confidence
Finding
The script embeds Chinese-only user-facing labels for quota windows, and additional Chinese output appears elsewhere in the file. This forces a specific language for all users without opt-in or documented locale justification, which matches the language/locale policy violation criteria.

Natural-Language Policy Violations

Medium
Confidence
98% confidence
Finding
The formatted output shown to users, including success, error, reset, model, account, and rate-limit messages, is written entirely in Chinese. There is no mechanism for language selection or indication that the skill is region- or locale-specific, so the file enforces a specific language by default.

Natural-Language Policy Violations

Medium
Confidence
95% confidence
Finding
This Python file contains multiple user-visible strings in Chinese, starting with the error message at L029 and continuing throughout the CLI flow. Because the skill forces one language for interaction without opt-in or justification, it creates a natural-language locale policy violation under the stated rules.

Intent-Code Divergence

Medium
Confidence
96% confidence
Finding
The docstring describes the script as a 'Fast local account/status query' that reads local auth profile metadata and prints a summary. However, the implementation later uses OAuth access tokens to call https://chatgpt.com/backend-api/wham/usage and retrieve live quota data, which is not a local-only operation.

subprocess module call

Medium
Category
Dangerous Code Execution
Content
state = load_json(STATE_PATH, {"version": 1})
    set_order_in_state(state, profile_id, profile_ids)
    try:
        out = subprocess.check_output([str(QUOTA_SCRIPT), "--json"], text=True, timeout=12)
        result = json.loads(out)
        if not result.get("ok"):
            return {"ok": False, "error": result.get("error", "quota_failed")}
Confidence
70% confidence
Finding
subprocess module calls execute external commands. Without careful input validation, this enables command injection.

Tainted flow: 'QUOTA_SCRIPT' from os.environ.get (line 22, credential/environment) → subprocess.check_output (code execution)

Medium
Category
Data Flow
Content
state = load_json(STATE_PATH, {"version": 1})
    set_order_in_state(state, profile_id, profile_ids)
    try:
        out = subprocess.check_output([str(QUOTA_SCRIPT), "--json"], text=True, timeout=12)
        result = json.loads(out)
        if not result.get("ok"):
            return {"ok": False, "error": result.get("error", "quota_failed")}
Confidence
96% confidence
Finding
QUOTA_SCRIPT is derived from an environment variable and then executed directly via subprocess.check_output. If an attacker can influence the environment in which this skill runs, they can point QUOTA_SCRIPT to an arbitrary executable and achieve code execution with the privileges of the agent, which is especially dangerous because this script also reads local auth/profile state and accesses OAuth tokens.

Natural-Language Policy Violations

Low
Confidence
96% confidence
Finding
The skill builds user-facing status messages entirely in Chinese for switch and no-candidate notifications. This forces a specific language/locale on recipients without offering any user selection or documenting a justified locale constraint.

Natural-Language Policy Violations

Low
Confidence
95% confidence
Finding
The file explicitly says it 'prints a short Chinese summary,' which imposes a specific language in natural-language text. Under the policy, forced language/locale behavior should be flagged unless the skill offers user opt-in or documents a justified regional constraint, neither of which is present here.

Static analysis

No suspicious patterns detected.