T09 · Insecure Skill Coding Practices
- Location
scripts/cs.sh:9- Finding
Environment-Controlled OAuth Endpoint Can Exfiltrate Authentication Credentials
- Content
View full analysis
- Remediation
View remediation
Security audit
Security checks for vulnerabilities and agentic risk
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.
scripts/cs.sh:9Environment-Controlled OAuth Endpoint Can Exfiltrate Authentication Credentials
scripts/cs.sh:80Pending OAuth State File Is Created Without Explicit Restrictive Permissions
Code accesses credential files (SSH keys, AWS credentials, etc.). This could indicate credential theft attempts.
- 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
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.
### 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
Remote code is downloaded and executed. This bypasses code review and could introduce malicious code.
## Never do
- `curl | bash`
- hidden proxy fallback
- writing unrelated config
- bloating `openclaw.json` with a growing Codex roster
Remote code is downloaded and executed. This bypasses code review and could introduce malicious code.
## Never do
- `curl | bash`
- hidden proxy fallback
- writing unrelated config
- bloating `openclaw.json` with a growing Codex roster
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.
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.
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.
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.
Data is being sent to an external URL. This could be legitimate telemetry or data exfiltration. Manual review is recommended.
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()
Data is being sent to an external URL. This could be legitimate telemetry or data exfiltration. Manual review is recommended.
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()
Data is being sent to an external URL. This could be legitimate telemetry or data exfiltration. Manual review is recommended.
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()
Data is being sent to an external URL. This could be legitimate telemetry or data exfiltration. Manual review is recommended.
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()
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.
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', '')