Back to skill

Security audit

birdx

Security checks for vulnerabilities and agentic risk

Overview

This Twitter/X CLI is purpose-aligned, but it handles browser session cookies unsafely and has a command-execution flaw, so users should review it carefully before installing.

Install only if you are comfortable with a local CLI reading and decrypting your Chrome X/Twitter session cookies, storing them in plaintext under your home directory, and installing live npm dependencies. Prefer a version that uses an explicit OAuth-style flow or OS-backed secret storage, validates usernames before shell use, pins dependencies, and provides a clear cache deletion and session revocation path.

Vulnerability Patterns
  • Unauthorized Access and Privilege EscalationObtains permissions beyond the task's legitimate needs
  • Insecure DependenciesIntroduces malicious components through unsafe dependency sources
  • 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
Findings (3)

T09 · Insecure Skill Coding Practices

Error
Location
scripts/birdx.js:613
Finding
Shell Command Injection Through an Unvalidated Username<![CDATA[ ## Vulnerability Details **File Location**: `scripts/birdx.js`, lines 613-617 **Vulnerability Type**: OS command injection **Risk Level**: Critical ### Vulnerable Code ```js async function resolveUsernameToId(username) { const handle = username.replace(/^@/, ''); try { const out = execSync(`bird user-tweets ${handle} -n 1 --json 2>/dev/null`, { timeout: 15000 }).toString().trim(); const data = JSON.parse(out); ``` ### Technical Analysis The `username` value originates from a positional command-line argument. Removing an optional leading `@` does not validate or escape the value. The resulting `handle` is interpolated directly into a command string passed to `execSync()`. Because the string form of `execSync()` invokes a shell, shell metacharacters contained in the username can introduce additional commands, pipelines, substitutions, or redirections. The vulnerable path is reachable through both the `followers` and `following` commands. The external command also redirects standard error through shell syntax, confirming that the command is deliberately interpreted by a shell rather than executed as a fixed executable with an argument array. ### Attack Path 1. An attacker persuades a user, automation agent, or service to invoke `birdx followers` or `birdx following` with an attacker-controlled username. 2. CLI parsing accepts the positional value without validating it as an X username. 3. `cmdFollowers()` or `cmdFollowing()` passes the value to `resolveUsernameToId()`. 4. `resolveUsernameToId()` interpolates the value into the `bird user-tweets ...` shell command. 5. Shell metacharacters in the supplied value alter the command structure. 6. The injected command executes with the privileges and environment of the user running `birdx`. For example, a value structurally equivalent to `validname; additional-command` would cause the shell to interpret the text following the semicolon as another command. ### Impact Assessment Successful ...[truncated 629 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. Do not pass user-controlled data through a shell. 2. Replace `execSync()` with `execFileSync()` or `spawnSync()` and provide each argument separately: ```js const { execFileSync } = require('child_process'); function validateHandle(username) { const handle = username.replace(/^@/, ''); if (!/^[A-Za-z0-9_]{1,15}$/.test(handle)) { throw new Error('Invalid X username'); } return handle; } const handle = validateHandle(username); const out = execFileSync( 'bird', ['user-tweets', handle, '-n', '1', '--json'], { encoding: 'utf8', timeout: 15000, stdio: ['ignore', 'pipe', 'ignore'], } ).trim(); ``` 3. Enforce X's username character and length constraints before invoking any external program. 4. Resolve and validate the intended executable path where practical, rather than relying entirely on `PATH`. 5. Add regression tests containing spaces, semicolons, command substitutions, redirections, pipes, and newline characters, verifying that every invalid value is rejected. ]]>

T05 · Unauthorized Access and Privilege Escalation

Error
Location
scripts/birdx.js:73
Finding
Excessive Browser Cookie Extraction and Plaintext Session Credential Storage<![CDATA[ ## Vulnerability Details **File Location**: `scripts/birdx.js`, lines 73-75, 197-217, 251-267, and 332-339 **Vulnerability Type**: Excessive credential access and insecure storage of session credentials **Risk Level**: High ### Vulnerable Code Plaintext persistence: ```js function saveCookies(cookies) { fs.mkdirSync(path.dirname(COOKIE_FILE), { recursive: true }); fs.writeFileSync(COOKIE_FILE, JSON.stringify({ ...cookies, savedAt: Date.now() }, null, 2)); } ``` Extraction of every nonempty X/Twitter cookie: ```js // Build full cookie string for x.com (reuse all cookies) const fullDb = new sqlite.DatabaseSync(tmpDb, { readonly: true }); const allRows = fullDb.prepare( "SELECT name, encrypted_value, value FROM cookies WHERE host_key LIKE '%x.com' OR host_key LIKE '%twitter.com'" ).all(); fullDb.close(); const cookiePairs = []; for (const row of allRows) { const encBuf = Buffer.isBuffer(row.encrypted_value) ? row.encrypted_value : Buffer.from(row.encrypted_value); let val = row.value || ''; if (!val && encBuf.length > 0) { val = decryptChromeCookie(encBuf, key, stripHashPrefix) || ''; } if (val) cookiePairs.push(`${row.name}=${val}`); } const cookieStr = cookiePairs.join('; '); return { cookieStr, ct0, authToken, source: 'chrome-disk' }; ``` CDP cookie collection: ```js const ws = new WebSocket(anyPage.webSocketDebuggerUrl); const timer = setTimeout(() => { ws.close(); reject(new Error('CDP timeout')); }, 10000); ws.on('open', () => { ws.send(JSON.stringify({ id: 1, method: 'Network.enable' })); ws.send(JSON.stringify({ id: 2, method: 'Network.getAllCookies' })); }); ws.on('message', (data) => { const msg = JSON.parse(data.toString()); if (msg.id === 2) { clearTimeout(timer); ws.close(); const xCookies = (msg.result?.cookies || []) .filter(c => c.domain.includes('x.com') || c.domain.includes('twitter.com')); const cookieStr = xCookies.map(c => `${c.name}=${c.value}`).join('; '); cons ...[truncated 3491 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. Minimize credential collection: - Query only the exact cookie names required for authenticated operations. - Do not construct or persist a complete X/Twitter cookie string. - Confirm whether only `auth_token` and `ct0` are necessary and omit every unrelated cookie. 2. Avoid broad CDP access: - Prefer a domain-scoped cookie retrieval mechanism where supported. - If CDP cannot avoid returning all cookies, isolate the operation in a minimal trusted process, immediately discard unrelated values, and document the temporary exposure. 3. Replace plaintext JSON storage with the macOS Keychain or another OS-backed credential manager. 4. If a file cache must be supported: - Create the directory with mode `0o700`. - Create and maintain the credential file with mode `0o600`. - Reject symlinked cache paths. - Use atomic creation with exclusive flags where possible. - Store only the minimum required fields. - Delete expired credentials instead of merely declining to load them. 5. Require explicit user confirmation before first-time browser credential extraction. Clearly explain which profile, domains, cookie names, destination endpoints, and retention period are involved. 6. Do not print token prefixes. Although only partial values are printed, logs should not contain any part of session credentials. 7. Provide a logout or cache-clearing command that securely removes locally persisted credentials and explains how to revoke the corresponding X session. ]]>

T08 · Insecure Dependencies

Warning
Location
scripts/install.sh:28
Finding
Unpinned and Integrity-Unverified npm Dependencies<![CDATA[ ## Vulnerability Details **File Location**: `scripts/install.sh`, lines 28-35 **Vulnerability Type**: Software supply-chain exposure **Risk Level**: Medium ### Vulnerable Code ```bash # Install npm deps echo "📦 Installing deps (ws, jsdom, x-client-transaction-id)..." mkdir -p "$MODULES_DIR" cd "$HOME/clawd" [ ! -f package.json ] && echo '{"name":"clawd","private":true}' > package.json npm install --prefix "$HOME/clawd" ws jsdom x-client-transaction-id --save-exact --silent echo "✅ Deps installed to $MODULES_DIR" ``` ### Technical Analysis The installer requests three packages without specifying reviewed version numbers and the project contains no reviewed lockfile in the supplied directory. The `--save-exact` option records the versions selected during that particular installation, but it does not pin versions in advance or guarantee that different users receive the same audited dependency graph. The installation also does not use `npm ci`, a committed lockfile, or independent integrity verification. npm package lifecycle scripts are not disabled. Consequently, package or transitive-dependency code selected at installation time may execute installation scripts and is later imported into the same Node.js process that handles decrypted browser credentials. This creates a significant trust-boundary concern because `jsdom`, `ws`, and `x-client-transaction-id`, along with their transitive dependencies, run with the user's filesystem and network privileges. ### Attack Path 1. A user follows `SKILL.md` or runs `scripts/install.sh`. 2. `npm install` resolves the current registry versions of the named packages and their transitive dependencies. 3. A compromised package release, compromised maintainer account, registry substitution, or malicious registry configuration supplies unexpected code. 4. npm lifecycle code may execute during installation, or the package code executes when loaded by `birdx.js`. 5. The dependency runs with the privileges of the ...[truncated 786 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. Pin direct dependencies to specific reviewed versions in `package.json`. 2. Generate, review, and commit a `package-lock.json` containing resolved versions and integrity hashes. 3. Replace the dynamic installation command with `npm ci` so installations fail if the lockfile and manifest differ. 4. Run dependency security and provenance reviews before updating pinned versions. 5. Document the expected npm registry and reject unexpected registry overrides in security-sensitive deployments. 6. Consider disabling lifecycle scripts with `--ignore-scripts` if the selected dependencies do not require them. 7. Install dependencies in a Skill-specific directory instead of the shared `$HOME/clawd` package tree to reduce dependency-confusion and cross-project modification risks. 8. Avoid silent installation during security-sensitive setup so users and operators can inspect npm warnings and failures. ]]>
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
  • Trigger AbuseOverly Broad Trigger, Shadow Command Trigger, Keyword Baiting Trigger
  • YARA SignaturesMalware Match, Webshell Match, Cryptominer Match
Findings (15)

Missing User Warnings

High
Confidence
98% confidence
Finding
The markdown describes reading Chrome cookies from disk and caching them locally, but does not present an explicit privacy or safety warning about the sensitivity of session cookies and the risks of local persistence. Session cookies can enable account access or impersonation if mishandled, and local caches increase exposure to theft, reuse, or accidental disclosure.

Missing User Warnings

High
Confidence
97% confidence
Finding
The tool writes decrypted X/Twitter session cookies to a predictable file under the user's home directory, creating a reusable local credential cache. If local filesystem protections are weak, backups are exposed, or another process/user can read the file, the session may be hijacked.

Credential Access

High
Category
Privilege Escalation
Content
// ── Chrome direct disk reader (no browser needed) ─────────────────────────────

/**
 * Derives the AES key from macOS Keychain (bird's A$ function).
 * Uses raw password string bytes (NOT base64-decoded) with PBKDF2.
 */
function deriveChromeCookieKey(password) {
Confidence
96% confidence
Finding
The section documents deriving a decryption key from the macOS Keychain for Chrome cookie decryption, which is a credential-access pattern commonly used to obtain reusable browser secrets. In this skill's context, that directly supports extraction of authenticated session material for another service.

Description-Behavior Mismatch

High
Confidence
99% confidence
Finding
The code explicitly retrieves the Chrome Safe Storage secret from the macOS Keychain, decrypts Chrome's cookie database, extracts X/Twitter session cookies, and persists them to disk for later reuse. That is credential harvesting behavior beyond a normal read-only Twitter CLI and creates reusable session material that could enable account access if the local cache is exposed or abused.

Missing User Warnings

High
Confidence
98% confidence
Finding
The code accesses the macOS Keychain to derive Chrome's cookie decryption key and then reads browser-stored credentials without a clear upfront warning or consent flow tailored to this sensitive operation. Accessing OS credential storage and decrypting browser secrets is highly sensitive and inconsistent with least surprise for a data-reading CLI.

YARA rule 'info_stealer': Information stealer patterns (credential harvesting, browser data theft) [malware]

High
Category
YARA Match
Content
p 32-byte random prefix if version >= 24 (bird's stripHashPrefix)
    if (stripHashPrefix && dec.length >= 32) dec = dec.slice(32);
    return dec.toString('utf8').replace(/^[\x00-\x1f]+/, '');
  } catch {
    return null;
  }
}

/**
 * Reads auth_token and ct0 directly from Chrome's SQLite cookie database.
 * No browser required — works while Chrome is running or closed.
 */
async function loadCookiesFromChrome() {
  // 1. Get Chrome Safe Storage password from macOS Keychain
  let pw;
  try {
    pw = execSync('security find-generic-password -w -s "Chrome Safe Storage" -a "Chrome"',
      { encoding: 'utf8', timeout: 5000 }).trim();
  } catch (e) {
    throw new Error(`Failed to read Chrome Safe Storage from Keychain: ${e.message}`);
  }
  if (!pw) throw new Error('Chrome Safe Storage password is empty');

  const key = deriveChromeCookieKey(pw);

  // 2. Locate and copy the Cookies database
  const chromeCookieDb = path.join(
    process.env.HOME,
    'Library/Application Support/G
Confidence
99% confidence
Finding
The YARA match is justified: the code contains multiple information-stealer characteristics, including obtaining a browser decryption key from Keychain, reading Chrome's cookies database, decrypting session cookies, and caching them for later use. Even if framed as convenience functionality, this behavior materially resembles credential-harvesting tooling.

Credential Access

High
Category
Privilege Escalation
Content
* No browser required — works while Chrome is running or closed.
 */
async function loadCookiesFromChrome() {
  // 1. Get Chrome Safe Storage password from macOS Keychain
  let pw;
  try {
    pw = execSync('security find-generic-password -w -s "Chrome Safe Storage" -a "Chrome"',
Confidence
99% confidence
Finding
This code executes the `security` tool to read the Chrome Safe Storage password from the macOS Keychain, enabling decryption of browser-stored cookies. That is direct credential access to OS-protected secrets and can facilitate unauthorized reuse of web sessions.

Credential Access

High
Category
Privilege Escalation
Content
pw = execSync('security find-generic-password -w -s "Chrome Safe Storage" -a "Chrome"',
      { encoding: 'utf8', timeout: 5000 }).trim();
  } catch (e) {
    throw new Error(`Failed to read Chrome Safe Storage from Keychain: ${e.message}`);
  }
  if (!pw) throw new Error('Chrome Safe Storage password is empty');
Confidence
91% confidence
Finding
This line is an error message referencing Keychain access failure, not additional credential-access behavior by itself. The real vulnerability is the surrounding logic that attempts the Keychain read, which is already captured elsewhere.

Credential Access

High
Category
Privilege Escalation
Content
// 2. Locate and copy the Cookies database
  const chromeCookieDb = path.join(
    process.env.HOME,
    'Library/Application Support/Google/Chrome/Default/Cookies'
  );
  if (!fs.existsSync(chromeCookieDb)) {
    throw new Error(`Chrome Cookies DB not found at ${chromeCookieDb}`);
Confidence
99% confidence
Finding
The code targets Chrome's on-disk cookies database in the user's profile, a classic browser credential store containing authenticated session material. Reading that database to recover `auth_token` and `ct0` enables reuse of the user's X/Twitter session outside the browser.

Lp3

Medium
Category
MCP Least Privilege
Confidence
96% confidence
Finding
The skill exposes capabilities that imply shell execution, network access, and environment interaction, but the manifest does not declare any tool scope or permission boundaries. In a skill that installs packages, symlinks binaries, reads browser cookies from disk, and performs live requests to X/Twitter, this omission prevents informed consent and weakens sandboxing and policy enforcement.

Vague Triggers

Medium
Confidence
93% confidence
Finding
The description encourages broad use ('Use when you need to fetch Twitter/X data...') without clearly constraining when the skill should be invoked or warning that it reads Chrome cookies from disk. That increases the chance an agent will invoke it in situations involving unrelated user data or without the user's informed approval for credential-adjacent access.

Intent-Code Divergence

Medium
Confidence
94% confidence
Finding
The header comment repeatedly states that no browser is required and that the tool works by reading Chrome cookies directly from disk. However, the code later implements `fetchCookiesViaCDP()` and uses it as a fallback authentication path, which depends on a running browser exposing a debugging endpoint.

Description-Behavior Mismatch

Medium
Confidence
93% confidence
Finding
When cached or disk cookies are unavailable, the tool silently falls back to extracting cookies from a live browser via CDP, expanding the credential-access surface without a strong, explicit opt-in at the time of use. This makes the behavior less transparent and can unexpectedly pull authenticated browser session data from a running browser context.

Context-Inappropriate Capability

Medium
Confidence
88% confidence
Finding
The code shells out to an external `bird` command using a username-derived value, which introduces unnecessary command-execution risk and a hidden dependency unrelated to the stated core purpose. Although the handle is somewhat constrained, invoking a shell for simple user resolution increases attack surface and can be abused if input validation or execution context changes.

Natural-Language Policy Violations

Medium
Confidence
99% confidence
Finding
The date formatter unconditionally calls toLocaleString with zh-CN and Asia/Shanghai, overriding the user's system locale and regional settings. This is a natural-language/locale policy concern because it imposes a specific language/locale presentation without offering a choice or documenting a justified regional restriction.