Back to skill

Security audit

Codex Switcher

Security checks for vulnerabilities and agentic risk

Overview

This skill appears purpose-built for Codex account switching, but it handles long-lived OAuth tokens and has an under-scoped endpoint override that could leak credentials in a tampered environment.

Review carefully before installing. Use only in a trusted local environment, unset CS_OAUTH_AUTHORIZE_URL and CS_OAUTH_TOKEN_URL unless you are deliberately testing, protect ~/.openclaw so only your user can read it, and treat all snapshot and backup files as account credentials.

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 (2)

T09 · Insecure Skill Coding Practices

Error
Location
scripts/cs.sh:9
Finding
Environment-Controlled OAuth Endpoint Can Exfiltrate Authentication Credentials<![CDATA[ ## Vulnerability Details **File Location**: `scripts/cs.sh:9`, `scripts/cs.sh:114-130`, `scripts/cs.sh:296-303`, and `scripts/cs.sh:344-351` **Vulnerability Type**: Arbitrary authentication endpoint override **Risk Level**: High ### Vulnerable Code ```bash TOKEN_URL="${CS_OAUTH_TOKEN_URL:-https://auth.openai.com/oauth/token}" ``` Authorization-code exchange: ```python body=urlencode({ 'grant_type':'authorization_code', 'client_id':pending['client_id'], 'code':code, 'code_verifier':pending['verifier'], 'redirect_uri':pending['redirect_uri'], }).encode() req=Request(token_url, data=body, method='POST', headers={ 'Content-Type':'application/x-www-form-urlencoded', 'Accept':'application/json', 'User-Agent':'cs/1.0' }) with urlopen(req, timeout=30) as resp: data=json.loads(resp.read().decode('utf-8','replace')) ``` Single-snapshot refresh: ```python resp = requests.post(token_url, data={ 'grant_type': 'refresh_token', 'refresh_token': data['refresh'], 'client_id': client_id, }, timeout=15) resp.raise_for_status() ``` Bulk snapshot refresh: ```python resp = requests.post(token_url, data={ 'grant_type': 'refresh_token', 'refresh_token': refresh, 'client_id': client_id, }, timeout=15) resp.raise_for_status() ``` ### Technical Analysis The default token endpoint is the official OpenAI OAuth endpoint. However, the `CS_OAUTH_TOKEN_URL` environment variable can replace it with an arbitrary URL without validation of the scheme, hostname, port, or path. The authorization-code flow sends the authorization code, PKCE verifier, client identifier, and redirect URI to the configured endpoint. The refresh flows send reusable refresh tokens to that endpoint. Consequently, a malicious or compromised execution environment can redirect sensitive OAuth material to an attacker-controlled service. This contradicts the documented security posture of using official OpenAI OAuth endpoints only. Endpoint config ...[truncated 1813 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. Remove `CS_OAUTH_TOKEN_URL` configurability from production code and use a fixed official endpoint: ```bash TOKEN_URL="https://auth.openai.com/oauth/token" ``` 2. If endpoint customization is required for controlled development or testing, place it behind an explicit development-only option that is disabled by default. 3. Before transmitting credentials, parse and validate the URL and require: - HTTPS - Exact hostname `auth.openai.com` - The expected port - Exact token endpoint path `/oauth/token` - No embedded username or password - No fragments or unexpected query parameters 4. Apply equivalent validation to `CS_OAUTH_AUTHORIZE_URL` so users cannot be directed to a spoofed authorization page. 5. Disable cross-origin redirects for requests carrying authorization codes, PKCE verifiers, or refresh tokens. If redirects are supported, revalidate the destination before following them and never forward sensitive request bodies to a different origin. 6. When an endpoint fails validation, terminate before reading or transmitting stored credentials. 7. Document any supported endpoint override as a dangerous testing feature and ensure cron jobs, services, and launchers sanitize authentication-related environment variables. ]]>

T09 · Insecure Skill Coding Practices

Warning
Location
scripts/cs.sh:80
Finding
Pending OAuth State File Is Created Without Explicit Restrictive Permissions<![CDATA[ ## Vulnerability Details **File Location**: `scripts/cs.sh:5`, `scripts/cs.sh:14`, and `scripts/cs.sh:80-87` **Vulnerability Type**: Insecure storage of temporary OAuth secrets **Risk Level**: Medium ### Vulnerable Code ```bash PENDING_FILE="$STATE_DIR/cs-add.pending.json" ``` ```bash mkdir -p "$SNAPSHOT_DIR" "$BACKUP_DIR" ``` ```python obj = { 'alias': alias, 'client_id': client_id, 'redirect_uri': redirect_uri, 'verifier': verifier, 'state': state, 'created_at': int(time.time() * 1000), } with open(pending_path, 'w', encoding='utf-8') as f: json.dump(obj, f, ensure_ascii=False, indent=2) f.write('\n') ``` ### Technical Analysis The pending OAuth file stores the PKCE verifier and OAuth state. These values are security-sensitive because the verifier is used to prove possession during the authorization-code exchange, while the state value protects the flow against callback substitution and cross-site request forgery. The file is created with Python's regular `open(..., 'w')`, so its effective permissions depend on the process umask and existing file state. No explicit `0600` permission is requested or enforced. The script also creates authentication snapshot and backup directories without explicitly enforcing mode `0700`. On a correctly configured private home directory with a restrictive umask, practical exposure may be limited. Nevertheless, the code itself does not guarantee confidentiality and can leave OAuth session material readable on shared systems or custom `OPENCLAW_STATE_DIR` locations. The file is also written directly rather than through a secure atomic temporary-file operation. This is inconsistent with the skill's documented rule requiring atomic writes for authentication state. ### Attack Path 1. The user runs the skill with a permissive umask, or sets `OPENCLAW_STATE_DIR` to a directory accessible by another local user. 2. `start_add` creates `cs-add.pending.json` without explicitly restricting its permission ...[truncated 1139 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. Enforce restrictive directory permissions before storing authentication material: ```bash mkdir -p "$SNAPSHOT_DIR" "$BACKUP_DIR" chmod 700 "$STATE_DIR" "$SNAPSHOT_DIR" "$BACKUP_DIR" ``` Avoid changing a shared state directory blindly; first verify that it is owned by the expected user. 2. Create the pending file atomically with mode `0600`. Use `tempfile.mkstemp` in the destination directory, write and flush the data, optionally call `fsync`, and then replace the destination with `os.replace`. 3. Explicitly apply `os.fchmod(fd, 0o600)` to the temporary file rather than depending on the process umask. 4. Before reading an existing pending file: - Reject symbolic links - Verify that it is a regular file - Verify expected ownership - Reject group-readable or world-readable permissions 5. Enforce a short validity period using `created_at` and reject stale pending flows. 6. Remove the pending file on success and on terminal failures where reuse is unsafe. 7. Apply equivalent ownership and permission checks to snapshots, backups, and the active authentication profile because those files contain access and refresh tokens. ]]>
Vulnerability Patterns
  • Data ExfiltrationExternal Transmission, Env Variable Harvesting, File System Enumeration
  • Privilege EscalationExcessive Permissions, Sudo/Root Execution, Credential Access
  • Supply ChainUnpinned Dependencies, External Script Fetching, Obfuscated Code
  • Rogue AgentSelf-Modification, Session Persistence
  • MCP Least PrivilegeUnderdeclared Capability, Wildcard Permission, Missing Permission Declaration
Findings (12)

Credential Access

High
Category
Privilege Escalation
Content
- temporary pending OAuth state files under `~/.openclaw/`

Treat all snapshot files as secrets.
Never expose full access tokens or refresh tokens in chat.

## Safety rules
Confidence
70% confidence
Finding
Code accesses credential files (SSH keys, AWS credentials, etc.). This could indicate credential theft attempts.

Credential Access

High
Category
Privilege Escalation
Content
### Refresh snapshots
1. Read snapshot `expires`
2. Skip snapshots not close to expiry
3. Use snapshot `refresh` token to get a new access token
4. Write refreshed credentials back into the same snapshot file

## Never do
Confidence
95% confidence
Finding
This flow is centered on handling refresh tokens to mint new access tokens, which is inherently sensitive because possession of the refresh token can enable long-lived account access. Although the README says not to print full tokens, it still prescribes persistent local storage and token reuse without specifying protections such as secure storage, scope minimization, revocation handling, or file-permission controls.

External Script Fetching

High
Category
Supply Chain
Content
## Never do

- `curl | bash`
- hidden proxy fallback
- writing unrelated config
- bloating `openclaw.json` with a growing Codex roster
Confidence
90% confidence
Finding
Remote code is downloaded and executed. This bypasses code review and could introduce malicious code.

External Script Fetching

High
Category
Supply Chain
Content
## Never do

- `curl | bash`
- hidden proxy fallback
- writing unrelated config
- bloating `openclaw.json` with a growing Codex roster
Confidence
90% confidence
Finding
Remote code is downloaded and executed. This bypasses code review and could introduce malicious code.

Lp3

Medium
Category
MCP Least Privilege
Confidence
94% confidence
Finding
The skill describes capabilities to read/write high-sensitivity auth files and perform networked OAuth/quota operations, but it declares no explicit tool scope or permissions boundary. In a system that relies on manifest-declared scopes, this creates an under-specified trust boundary and increases the chance the skill is invoked with broader file and network access than reviewers or users expect.

Session Persistence

Medium
Category
Rogue Agent
Content
2. sign in in browser
3. run `cs add --apply '<callback-url>' [alias]`
4. if alias was omitted, derive it from the email automatically
5. create a new snapshot file for that account

### `cs refresh <alias>`
Force-refresh one snapshot using its refresh token.
Confidence
89% confidence
Finding
The skill is explicitly designed to persist OAuth snapshots and refresh them over time, which creates long-lived local session material including refresh tokens. Even though this is core functionality, persistent credential storage materially raises compromise impact: theft of snapshot files can enable account takeover or silent token renewal across multiple Codex accounts.

Missing User Warnings

Medium
Confidence
91% confidence
Finding
The document explicitly instructs implementers to store OAuth access and refresh tokens in local snapshot files and to modify the active auth profile, but it does not require user consent, secure file permissions, encryption, or a warning that these files are highly sensitive. In a credential-management skill, this omission increases the chance of unsafe implementations that expose reusable authentication material or silently switch active identities.

External Transmission

Medium
Category
Data Exfiltration
Content
payload_b64 = access.split('.')[1]
payload_b64 += '=' * (-len(payload_b64) % 4)
payload = json.loads(base64.urlsafe_b64decode(payload_b64.encode()).decode())
auth_claim = payload.get('https://api.openai.com/auth', {}) if isinstance(payload, dict) else {}
profile_claim = payload.get('https://api.openai.com/profile', {}) if isinstance(payload, dict) else {}
account_id = str(auth_claim.get('chatgpt_account_id', '')).strip()
email = str(profile_claim.get('email', '')).strip()
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
payload_b64 = access.split('.')[1]
payload_b64 += '=' * (-len(payload_b64) % 4)
payload = json.loads(base64.urlsafe_b64decode(payload_b64.encode()).decode())
auth_claim = payload.get('https://api.openai.com/auth', {}) if isinstance(payload, dict) else {}
profile_claim = payload.get('https://api.openai.com/profile', {}) if isinstance(payload, dict) else {}
account_id = str(auth_claim.get('chatgpt_account_id', '')).strip()
email = str(profile_claim.get('email', '')).strip()
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
payload_b64 = access.split('.')[1]
payload_b64 += '=' * (-len(payload_b64) % 4)
payload = json.loads(base64.urlsafe_b64decode(payload_b64.encode()).decode())
auth_claim = payload.get('https://api.openai.com/auth', {}) if isinstance(payload, dict) else {}
profile_claim = payload.get('https://api.openai.com/profile', {}) if isinstance(payload, dict) else {}
account_id = str(auth_claim.get('chatgpt_account_id', '')).strip()
email = str(profile_claim.get('email', '')).strip()
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
payload_b64 = access.split('.')[1]
payload_b64 += '=' * (-len(payload_b64) % 4)
payload = json.loads(base64.urlsafe_b64decode(payload_b64.encode()).decode())
auth_claim = payload.get('https://api.openai.com/auth', {}) if isinstance(payload, dict) else {}
profile_claim = payload.get('https://api.openai.com/profile', {}) if isinstance(payload, dict) else {}
account_id = str(auth_claim.get('chatgpt_account_id', '')).strip()
email = str(profile_claim.get('email', '')).strip()
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
payload = access.split('.')[1]
        payload += '=' * (-len(payload) % 4)
        obj = json.loads(base64.urlsafe_b64decode(payload.encode()).decode())
        email = ((obj.get('https://api.openai.com/profile') or {}).get('email') or '-')
    headers = {
        'Authorization': f'Bearer {access}',
        'ChatGPT-Account-Id': prof.get('accountId', '')
Confidence
88% confidence
Finding
While the matched line itself is just local JWT parsing, the surrounding quota feature immediately uses the extracted active token to make an authenticated request to https://chatgpt.com/backend-api/wham/usage. This sends a bearer token and account identifier to an external service, which is expected for functionality but still security-relevant because a local skill handling multiple account snapshots can expose or misuse tokens if run in an untrusted context.

Static analysis

No suspicious patterns detected.