T09 · Insecure Skill Coding Practices
- Location
pw.js:119- Finding
Telegram Tweet Approval Is Not Bound to the Reviewed Draft or Session
- Content
View full analysis
Vulnerability Details
File Location:
pw.js:119-126andSKILL.md:63-75
Vulnerability Type: Shared mutable approval state / confused-deputy vulnerability
Risk Level: MediumVulnerable Code
js const workspace = process.env.OPENCLAW_WORKSPACE || path.join( process.env.HOME || process.env.USERPROFILE || '.', '.openclaw', 'workspace' ); const pendingDir = path.join(workspace, '.cdp-browser'); const pendingPath = path.join(pendingDir, 'pending-tweet.json'); if (!fs.existsSync(pendingDir)) { fs.mkdirSync(pendingDir, { recursive: true }); } fs.writeFileSync( pendingPath, JSON.stringify({ text: draftText, tabId, timestamp: Date.now() }, null, 2) );The approval instructions use a constant callback and direct the Agent to trust the shared file:
markdown --buttons '[[{"text":"Confirm Post","callback_data":"cdp:tweet:confirm"}]]' On confirm: - Read `~/.openclaw/workspace/.cdp-browser/pending-tweet.json` for `text` and `tabId` - Run `tweet-post <tabId> --confirm "<text>"`Technical Analysis
Every pending draft in a workspace is written to the same
pending-tweet.jsonpath. A later draft silently overwrites the earlier draft. The stored state contains only the tweet text, tab ID, and timestamp; it is not bound to the Telegram chat, approving user, Agent session, confirmation message, or a unique approval nonce.The confirmation button also always uses the same
cdp:tweet:confirmcallback value. Therefore, a confirmation event does not identify the specific draft that the user reviewed. The documented handler instead reads whatever content happens to occupy the shared pending file at confirmation time.There is also no documented expiration check, one-time atomic consumption, content-hash verification, or integrity protection. Any concurrent session—or another process with write access to the workspace—can replace the approved state before it is consumed.
Attack Path
- A legitimate user creates draft A ...[truncated 1715 chars]
- Remediation
View remediation
Remediation Suggestions
- Generate a cryptographically random approval identifier for every draft, for example with
crypto.randomUUID()orcrypto.randomBytes(). - Store each pending draft separately using that identifier rather than a single shared filename.
- Bind the stored record to:
- Telegram chat ID;
- approving user ID where available;
- OpenClaw session or conversation ID;
- confirmation message ID;
- browser tab ID;
- exact tweet text or a cryptographic hash of it;
- creation and expiration times.
- Include the unique approval identifier in
callback_data, such ascdp:tweet:confirm:<approval-id>. - On confirmation, verify that the callback sender, chat, session, message, draft hash, and tab match the stored record.
- Enforce a short expiration period and reject expired approvals.
- Atomically consume or rename the approval record before posting so the same approval cannot be replayed.
- Create the state directory and files with restrictive permissions, such as directory mode
0700and file mode0600. - Avoid trusting tweet text or a tab ID read from mutable shared state without validating its association with the confirmation event.
- Display the exact text or its digest at confirmation time and verify it again immediately before posting.
- Add tests covering concurrent drafts, cross-chat confirmation, callback replay, expired approvals, and state replacement between review and confirmation.
- Generate a cryptographically random approval identifier for every draft, for example with
