Back to skill

Security audit

Codex Auth

Security checks for vulnerabilities and agentic risk

Overview

This OAuth helper is mostly purpose-aligned, but it needs review because it handles long-lived login tokens using unsafe temporary files and can restart the local gateway in the background.

Install only if you are comfortable with this skill writing OpenAI OAuth access and refresh tokens into local OpenClaw auth files and potentially restarting the gateway. Avoid queue mode until token staging is moved out of /tmp, permissions are forced to 0600, queued payloads are deleted after use, state validation rejects missing state, and callback URLs are not passed through shell command arguments.

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

T09 · Insecure Skill Coding Practices

Error
Location
scripts/codex_auth.py:109
Finding
OAuth secrets are stored in predictably named temporary files without enforced restrictive permissions<![CDATA[ ## Vulnerability Details **File Location**: `scripts/codex_auth.py:109-115`, with sensitive callers at `scripts/codex_auth.py:151-162` and `scripts/codex_auth.py:337-339` **Vulnerability Type**: Insecure temporary-file handling and plaintext credential storage **Risk Level**: High ### Vulnerable Code ```python def write_json_atomic(path, data): tmp = path + ".tmp" with open(tmp, "w", encoding="utf-8") as f: json.dump(data, f, indent=2) f.write("\n") os.replace(tmp, path) ``` This helper is used to store pending PKCE credentials: ```python def save_pending(profile_id, verifier, state): pending = read_json(PENDING_PATH) pending[profile_id] = { "verifier": verifier, "state": state, "createdAt": int(time.time() * 1000) } os.makedirs(os.path.dirname(PENDING_PATH), exist_ok=True) write_json_atomic(PENDING_PATH, pending) ``` It is also used to store access and refresh tokens for queued application: ```python payload_path = f"/tmp/openclaw/codex-auth-apply-{profile_id.replace(':','_')}.json" write_json_atomic(payload_path, {"profile": profile_id, "tokens": tokens}) ``` ### Technical Analysis The temporary and destination files are created using the process's current `umask`; the code does not explicitly enforce mode `0600`. If the environment has a permissive `umask`, PKCE verifiers, OAuth state values, access tokens, and refresh tokens may be readable by other local users. The queued payload has a predictable filename derived from the profile ID. The generic writer also uses a predictable `path + ".tmp"` intermediate file and does not use exclusive creation, `O_NOFOLLOW`, ownership checks, or file-type validation. In a shared or insufficiently protected temporary directory, these properties create opportunities for local file disclosure, symlink attacks, and race conditions. Queued token payloads are not deleted after `apply_with_gateway_restart()` reads them, so refresh tokens ...[truncated 1365 chars]
Remediation
<![CDATA[ ## Remediation Suggestions - Store sensitive runtime state in a user-specific directory owned by the current user and set its mode to `0700`. - Create sensitive files with `os.open()` using `O_CREAT | O_EXCL | O_NOFOLLOW` and mode `0600`. - Generate cryptographically random queued-payload filenames rather than deriving them from profile IDs. - Reject symbolic links and verify the file owner, type, and permissions before reading or replacing a file. - Explicitly set the final file mode to `0600`, independent of the process `umask`. - Remove queued payloads in a `finally` block immediately after loading them. - Keep access and refresh tokens in memory where possible. - Use `fsync()` on the temporary file and parent directory when durability is required. ]]>

T09 · Insecure Skill Coding Practices

Error
Location
scripts/codex_auth.py:186
Finding
Atomic replacement invalidates auth-profile locking and can weaken credential-file permissions<![CDATA[ ## Vulnerability Details **File Location**: `scripts/codex_auth.py:186-218` **Vulnerability Type**: Incorrect file-locking design and unsafe credential-store replacement **Risk Level**: High ### Vulnerable Code ```python def update_auth_profile(auth_path, profile_id, credentials): os.makedirs(os.path.dirname(auth_path), exist_ok=True) backup = backup_file(auth_path, "before-codex-auth") # lock + read existing with open(auth_path, "a+", encoding="utf-8") as f: fcntl.flock(f, fcntl.LOCK_EX) f.seek(0) try: raw = f.read().strip() store = json.loads(raw) if raw else {} except Exception: store = {} store.setdefault("profiles", {}) store.setdefault("usageStats", {}) store.setdefault("order", {}) store["profiles"][profile_id] = { "provider": "openai-codex", "type": "oauth", "access": credentials["access"], "refresh": credentials["refresh"], "expires": credentials["expires"], "accountId": credentials["accountId"], } provider_order = store["order"].get("openai-codex") if not isinstance(provider_order, list): provider_order = [] if profile_id not in provider_order: provider_order.append(profile_id) store["order"]["openai-codex"] = provider_order write_json_atomic(auth_path, store) fcntl.flock(f, fcntl.LOCK_UN) return backup ``` ### Technical Analysis `flock()` applies to the inode referenced by the opened file descriptor. The code locks `auth_path`, but `write_json_atomic()` writes a separate temporary file and then calls `os.replace()`. The path consequently points to a new inode while the lock remains associated with the old inode. A concurrent process can open the replacement file and acquire a lock even though the first process still believes it holds exclusive coordination. Multiple w ...[truncated 1576 chars]
Remediation
<![CDATA[ ## Remediation Suggestions - Coordinate writers through a separate, stable lock file whose inode is never replaced. - Acquire that lock before reading the credential store and retain it until after the replacement and directory synchronization are complete. - Create the temporary credential file with mode `0600` and verify its owner and regular-file type. - Preserve restrictive ownership and permissions from an existing credential store when appropriate. - Write and flush the complete JSON document, call `fsync()` on the file, atomically replace the target, and call `fsync()` on the parent directory. - Avoid silently replacing malformed JSON with an empty store; fail safely and preserve the existing file for recovery. - Add concurrency tests that start multiple profile updates simultaneously and verify that no update is lost. ]]>

T09 · Insecure Skill Coding Practices

Warning
Location
scripts/codex_auth.py:324
Finding
OAuth state verification fails open when callback state is omitted<![CDATA[ ## Vulnerability Details **File Location**: `scripts/codex_auth.py:324-326` **Vulnerability Type**: Incomplete OAuth state validation **Risk Level**: Medium ### Vulnerable Code ```python if pending.get("state") and state and pending["state"] != state: print(json.dumps({"ok": False, "error": "state_mismatch"}, indent=2)) sys.exit(2) ``` ### Technical Analysis The condition rejects a callback only if an expected state exists, a supplied state exists, and the two values differ. If the callback omits `state`, the condition evaluates to false and token exchange proceeds. OAuth state is intended to bind the callback to the authorization flow initiated by the client and defend against cross-site request forgery and flow confusion. Because this Skill always creates and stores a state value, its absence in the callback should be treated as an authentication failure rather than as an optional condition. PKCE substantially limits direct authorization-code substitution because the exchanged code must match the pending verifier. Nevertheless, PKCE does not justify fail-open state validation, and accepting state-less callback input unnecessarily weakens the OAuth transaction boundary. ### Attack Path 1. The victim starts an OAuth flow, causing the Skill to store a PKCE verifier and expected state. 2. An attacker or confused caller supplies callback input containing an authorization code but no state value. 3. `parse_callback_input()` returns the code with `state` set to `None`. 4. The state-mismatch condition is bypassed because the supplied state is false. 5. The Skill attempts token exchange using the pending verifier rather than rejecting the malformed callback. 6. If the supplied code is valid for that PKCE transaction, the callback is accepted without verifying its required flow-binding value. ### Impact Assessment The flaw weakens CSRF and authorization-flow integrity. Practical exploitation requires an authorization code compatible with the ...[truncated 273 chars]
Remediation
<![CDATA[ ## Remediation Suggestions - Require `state` whenever a pending flow contains an expected state. - Reject callbacks with missing, empty, duplicate, or malformed state parameters. - Compare the supplied and expected values using `hmac.compare_digest()`. - Consume and delete pending flow state after a successful exchange. - Apply an expiration time to pending flows and reject stale entries. - Consider deleting pending entries after repeated invalid callback attempts to limit replay and guessing opportunities. A safe validation pattern is: ```python import hmac expected_state = pending.get("state") if not expected_state or not state or not hmac.compare_digest(expected_state, state): print(json.dumps({"ok": False, "error": "invalid_state"}, indent=2)) sys.exit(2) ``` ]]>

T09 · Insecure Skill Coding Practices

Warning
Location
scripts/codex_auth.py:286
Finding
Sensitive OAuth callback credentials are accepted through command-line arguments<![CDATA[ ## Vulnerability Details **File Location**: `scripts/codex_auth.py:286`; documented usage at `SKILL.md:36-42` **Vulnerability Type**: Sensitive information exposure through process arguments and shell history **Risk Level**: Medium ### Vulnerable Code ```python s_finish = sub.add_parser("finish") s_finish.add_argument("--profile", default="default") s_finish.add_argument("--callback-url", required=True) s_finish.add_argument("--auth-path", default=AUTH_PATH_DEFAULT) s_finish.add_argument("--queue-apply", action="store_true", help="Queue stop/write/start apply script in background") ``` The documented invocation places the complete callback URL on the command line: ```bash python3 skills/codex-auth/scripts/codex_auth.py finish --profile default --callback-url "http://localhost:1455/auth/callback?code=...&state=..." ``` ### Technical Analysis The callback URL contains an OAuth authorization code and state value. Requiring it as a command-line argument can expose those values through shell history, process listings, process-accounting systems, terminal session recording, diagnostic collection, wrapper scripts, and orchestration telemetry. Although the implementation does not print the callback URL in its JSON output, avoiding output alone is insufficient. The documented interface places the sensitive value into operating-system and shell metadata before the Python process handles it. This conflicts with the stated policy that callback URLs must be treated as sensitive. Authorization codes are short-lived and PKCE-bound, which limits the exploitation window. They nevertheless remain credentials and should not be placed in broadly observable channels. ### Attack Path 1. The victim follows the documented command and supplies the full callback URL using `--callback-url`. 2. The shell records the command in history, or a local observer inspects the process command line while it is running. 3. The attacker obtains the authorization code and state. 4. ...[truncated 770 chars]
Remediation
<![CDATA[ ## Remediation Suggestions - Do not require the callback URL as a normal command-line argument. - Read it from protected standard input or an interactive prompt. - Prefer implementing a localhost callback listener that receives the redirect directly and validates the exact path, state, and PKCE transaction. - If file-based input is necessary, require a user-owned mode-`0600` file and delete it after reading. - Update `SKILL.md` so examples do not place authorization codes or callback URLs in shell commands. - Warn users that callback URLs must not be pasted into chat, shell history, logs, or telemetry-enabled interfaces. - Retain output redaction and ensure exception messages never include the supplied callback URL. ]]>
Vulnerability Patterns
  • Data ExfiltrationExternal Transmission, Env Variable Harvesting, File System Enumeration
  • Privilege EscalationExcessive Permissions, Sudo/Root Execution, Credential Access
  • Excessive AgencyUnrestricted Tool Access, Autonomous Decision Making, Scope Creep
  • Behavioral ASTexec() Call, eval() Call, Dynamic Import
  • MCP Least PrivilegeUnderdeclared Capability, Wildcard Permission, Missing Permission Declaration
Findings (12)

Credential Access

High
Category
Privilege Escalation
Content
## Denied operations
- No remote shell execution (`curl|bash`, `wget|sh`).
- No `sudo`, SSH, package manager, or system mutation actions.
- No disclosure of full callback query params, access tokens, or refresh tokens.
Confidence
70% confidence
Finding
Code accesses credential files (SSH keys, AWS credentials, etc.). This could indicate credential theft attempts.

Tp4

High
Category
MCP Tool Poisoning
Confidence
98% confidence
Finding
The skill is presented as a deprecated shim that should redirect users elsewhere, but the content describes a fully active OAuth flow that exchanges tokens, writes auth data, and can restart the gateway. This mismatch is dangerous because users and automated governance may treat it as low-risk or inactive while it still performs privileged operations on sensitive credentials and local service state.

Context-Inappropriate Capability

High
Confidence
93% confidence
Finding
The skill can queue a background apply job that writes tokens to disk and controls the gateway lifecycle, which is far more powerful than its stated deprecated shim purpose suggests. In context, this mismatch is dangerous because users or callers may reasonably expect an auth helper, not a tool that creates persistence artifacts and asynchronously changes service state.

Lp3

Medium
Category
MCP Least Privilege
Confidence
89% confidence
Finding
The skill declares no explicit tool scope or permission boundaries, yet the documented behavior includes shell execution, network access, and reads/writes of sensitive auth state. In an agent ecosystem, missing permission declarations weakens reviewability and enforcement, making it easier for a seemingly simple skill to perform higher-risk actions than operators expect.

External Transmission

Medium
Category
Data Exfiltration
Content
TOKEN_URL = "https://auth.openai.com/oauth/token"
REDIRECT_URI = "http://localhost:1455/auth/callback"
SCOPE = "openid profile email offline_access"
JWT_CLAIM_PATH = "https://api.openai.com/auth"
OPENCLAW_CONFIG_PATH = str(Path.home() / ".openclaw" / "openclaw.json")
BACKUP_DIR = "/tmp/openclaw/safety-backups"
APPLY_STATUS_PATH = "/tmp/openclaw/codex-auth-apply-last.json"
Confidence
60% confidence
Finding
Data is being sent to an external URL. This could be legitimate telemetry or data exfiltration. Manual review is recommended.

subprocess module call

Medium
Category
Dangerous Code Execution
Content
def run_cmd(cmd):
    return subprocess.run(cmd, capture_output=True, text=True)


def apply_with_gateway_restart(payload_path, auth_path):
Confidence
70% confidence
Finding
subprocess module calls execute external commands. Without careful input validation, this enables command injection.

Description-Behavior Mismatch

Medium
Confidence
89% confidence
Finding
This deprecated compatibility/auth script performs active system reconfiguration, modifies persistent auth/config files, and stops/starts the gateway service. Those capabilities exceed a minimal auth-flow shim and increase the chance of unauthorized persistence, outages, or unsafe rollback behavior if the script is triggered unexpectedly or with manipulated inputs.

Missing User Warnings

Medium
Confidence
86% confidence
Finding
The apply path stops and restarts the gateway service as part of credential application without an immediate user confirmation in the action-performing code path. In a deprecated shim, undisclosed service interruption is risky because it can disrupt workloads and be abused as an availability-impacting action under the guise of authentication.

Missing User Warnings

Medium
Confidence
92% confidence
Finding
The queued apply path writes OAuth access and refresh tokens into a payload file under /tmp/openclaw before launching a background job. Temporary directories are a poor place for sensitive secrets because they may be exposed to other local users/processes, persist longer than intended, and are not accompanied by clear user disclosure in this path.

subprocess module call

Medium
Category
Dangerous Code Execution
Content
args.auth_path,
            ]
            with open("/tmp/openclaw/codex-auth-apply.log", "a", encoding="utf-8") as lf:
                subprocess.Popen(cmd, stdout=lf, stderr=lf, stdin=subprocess.DEVNULL)
            print(json.dumps({
                "ok": True,
                "profile": profile_id,
Confidence
81% confidence
Finding
The code spawns a detached background process that later applies configuration and restarts the gateway, using attacker-influenced inputs such as --auth-path and a payload file in /tmp. Although it does not use a shell, background execution reduces visibility and control, and in this deprecated auth shim context it expands the blast radius beyond simple authentication.

Missing User Warnings

Medium
Confidence
95% confidence
Finding
The script persists access and refresh tokens to the auth profile file without an explicit disclosure or consent step in this code path. Persisting long-lived credentials is expected for OAuth tooling, but doing so silently in a deprecated shim increases surprise and reduces the user's ability to assess local secret-handling risk.

Intent-Code Divergence

Low
Confidence
78% confidence
Finding
The comment says the non-queued path performs an immediate write with 'no restart,' but the same branch calls ensure_profile_declared_in_config and then reports restart_required and a config note telling the user to restart the gateway. That documentation understates the operational impact of the path and contradicts the emitted result semantics.

Static analysis

No suspicious patterns detected.