Back to skill

Security audit

X Bookmark Triage

Security checks for vulnerabilities and agentic risk

Overview

The skill is purpose-aligned but needs review because it can automatically remove X bookmarks and has unsafe token-handling and failure-ordering issues.

Review before installing. Use read-only X scopes or pass --no-unbookmark until deletion is opt-in and guarded by confirmed Discord delivery. Store OAuth tokens carefully, restrict data file permissions, and avoid running the .env-sourcing wrapper against any workspace you do not fully trust.

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/bookmark-poll.js:33
Finding
OAuth token cache may be created with overly permissive filesystem permissions## Vulnerability Details **File Location**: `scripts/bookmark-poll.js:33-38`; duplicate vulnerable write at `scripts/backlog-sweep.js:94-95` **Vulnerability Type**: Sensitive credential storage with permissions inherited from the process umask **Risk Level**: High ### Vulnerable Code `scripts/bookmark-poll.js:33-38`: ```js function saveTokenCache(data) { fs.mkdirSync(path.dirname(TOKEN_CACHE), { recursive: true }); fs.writeFileSync(TOKEN_CACHE, JSON.stringify({ ...data, cached_at: Date.now() }, null, 2)); } ``` `scripts/backlog-sweep.js:94-95`: ```js fs.mkdirSync(path.dirname(TOKEN_CACHE), { recursive: true }); fs.writeFileSync(TOKEN_CACHE, JSON.stringify({ ...data, cached_at: Date.now() }, null, 2)); ``` ### Technical Analysis Both scripts persist the complete OAuth token response to `data/x-oauth2-token-cache.json` without specifying a restrictive file mode. The response contains an X access token and may also contain a refresh token. When the cache file is first created, its permissions are determined by the process umask. Under a common `022` umask, the resulting file may be readable by other local users. This behavior contradicts the security claim in `README.md` that OAuth tokens are stored with mode `0o600`. Although `scripts/x-oauth2-authorize.js` and the rotated-token write explicitly use `0o600`, subsequent cache creation in these two polling scripts does not provide the same protection. If the file already exists with insecure permissions, adding a mode only to later writes is insufficient because opening an existing file does not necessarily replace its mode. Existing cache files must also be explicitly repaired. ### Attack Path 1. A user configures valid X OAuth credentials and runs `bookmark-poll.js` or `backlog-sweep.js`. 2. No token cache currently exists, so the script creates `data/x-oauth2-token-cache.json`. 3. The process uses a permissive umask, causing the file t ...[truncated 1112 chars]
Remediation
## Remediation Suggestions 1. Create the data directory with owner-only permissions and explicitly set token files to `0o600`: ```js function saveTokenCache(data) { fs.mkdirSync(path.dirname(TOKEN_CACHE), { recursive: true, mode: 0o700 }); fs.writeFileSync( TOKEN_CACHE, JSON.stringify({ ...data, cached_at: Date.now() }, null, 2), { mode: 0o600 } ); fs.chmodSync(TOKEN_CACHE, 0o600); } ``` 2. Apply the same correction in `backlog-sweep.js`. 3. Repair existing installations during startup by checking and resetting the cache mode with `fs.chmodSync`. 4. Prefer atomic credential writes: write to a randomly named owner-only file in the same directory, call `fsync`, set mode `0o600`, and rename it over the destination. 5. Store refresh tokens in an operating-system credential store or dedicated secrets manager where practical. 6. Add a setup check that rejects or warns about token files readable by group or other users. 7. Update documentation so its `0o600` claim is verified by all token-writing paths.

T09 · Insecure Skill Coding Practices

Error
Location
scripts/triage-url.js:340
Finding
Discord delivery failure is treated as successful and can cause permanent bookmark deletion## Vulnerability Details **File Location**: `scripts/triage-url.js:340-345`, with destructive callers at `scripts/bookmark-poll.js:236-245` and `scripts/backlog-sweep.js:257-263` **Vulnerability Type**: Incorrect success signaling and unsafe ordering of state-changing operations **Risk Level**: High ### Vulnerable Code `scripts/triage-url.js:340-345` marks the URL as processed before confirming delivery: ```js const card = buildCard(triage, fetched, url); // Mark as seen BEFORE Discord post — ensures durability even if post fails markSeen(url); const posted = await postToDiscord(card); ``` The script only emits an optional event when `posted` is true. When `posted` is false, it reaches the end of the asynchronous entry point without setting a nonzero exit code. Node.js therefore normally exits with status zero. `scripts/bookmark-poll.js:236-245` interprets that zero exit status as successful triage and removes the source bookmark: ```js } else if (result.status === 0) { triaged++; // Unbookmark after successful triage if (bookmark.tweetId && userId) { const deleted = deleteBookmark(accessToken, userId, bookmark.tweetId); if (deleted) { unbookmarked++; console.log(`[bookmark-poll] 🗑️ Unbookmarked: ${bookmark.tweetId}`); } else { console.warn(`[bookmark-poll] ⚠️ Unbookmark failed for: ${bookmark.tweetId}`); } } ``` `scripts/backlog-sweep.js:257-263` contains equivalent behavior: ```js } else if (result.status === 0) { totalTriaged++; if (!NO_UNBOOKMARK && bookmark.tweetId) { if (deleteBookmark(accessToken, userId, bookmark.tweetId)) { totalUnbookmarked++; console.log(`[backlog-sweep] 🗑️ Unbookmarked: ${bookmark.tweetId}`); } } } ``` ### Technical Analysis The pipeline uses child-process exit status as its success contract. However, `triage-url.js` does not make confirmed Discord delivery a condit ...[truncated 2329 chars]
Remediation
## Remediation Suggestions 1. Treat confirmed Discord delivery as part of the success condition: ```js const card = buildCard(triage, fetched, url); const posted = await postToDiscord(card); if (!posted) { console.error('[triage-url] Discord delivery failed'); process.exitCode = 1; return; } markSeen(url); ``` 2. Mark a URL as seen only after Discord returns a valid message ID. 3. Return a machine-readable result from `triage-url.js`, such as: ```json { "triaged": true, "posted": true, "message_id": "123", "safe_to_unbookmark": true } ``` Parent scripts should delete a bookmark only when `safe_to_unbookmark` is explicitly true rather than relying solely on exit status or matching console text. 4. Separate delivery states such as `pending`, `posted`, and `failed`. Persist pending work in a retry queue rather than treating it as seen. 5. Preserve the source bookmark whenever classification or delivery fails. 6. For low-tier items intentionally suppressed from Discord, require an explicit configuration option before deleting them; successful classification alone should not silently imply successful capture. 7. Make unbookmarking opt-in by default or use read-only OAuth without `bookmark.write` unless automatic removal is explicitly enabled. 8. Add tests covering invalid Discord tokens, inaccessible channels, network failures, rate-limit exhaustion, malformed API responses, and process timeouts. Each test should verify that the X deletion endpoint is not invoked.
Vulnerability Patterns
  • Privilege EscalationExcessive Permissions, Sudo/Root Execution, Credential Access
  • Tool MisuseTool Parameter Abuse, Chaining Abuse, Unsafe Defaults
  • MCP Tool PoisoningHidden Instructions, Unicode Deception, Parameter Description Injection
  • Prompt InjectionInstruction Override, Hidden Instructions, Exfiltration Commands
  • Data ExfiltrationExternal Transmission, Env Variable Harvesting, File System Enumeration
Findings (104)

Tp4

High
Category
MCP Tool Poisoning
Confidence
95% confidence
Finding
The skill description does not clearly disclose that it performs an interactive OAuth flow with a localhost callback server, browser launch, and token caching. Those behaviors materially expand the trust boundary because they create local listeners and store long-lived credentials on disk.

Tp4

High
Category
MCP Tool Poisoning
Confidence
98% confidence
Finding
The skill description does not clearly disclose that it performs an interactive OAuth flow with a localhost callback server, browser launch, and token caching. Those behaviors materially expand the trust boundary because they create local listeners and store long-lived credentials on disk.

Tool Parameter Abuse

High
Category
Tool Misuse
Content
Discord #knowledge-intake  (structured card)
                │
                ▼
    X API DELETE /bookmarks/:id  (unbookmark)
```

## Prerequisites
Confidence
95% confidence
Finding
The skill performs `DELETE /bookmarks/:id`, which is a destructive API action against the user's X account. In context, this is more dangerous because the deletion is tied to automated polling and dedup logic, so accidental invocation or misclassification can silently remove a user's bookmark backlog at scale.

Ae1

High
Category
analysis-evasion
Content
t env var `KNOWLEDGE_INTAKE_CHANNEL_ID=<your-channel-id>` (preferred), OR edit `scripts/triage-url.js` line 19 to hardcode it.
Confidence
100% confidence
Finding
Referenced artifact was not completely inspected

Ae1

High
Category
analysis-evasion
Content
t env var `KNOWLEDGE_INTAKE_CHANNEL_ID=<your-channel-id>` (preferred), OR edit `scripts/triage-url.js` line 19 to hardcode it.
Confidence
100% confidence
Finding
Referenced artifact was not completely inspected

Ae1

High
Category
analysis-evasion
Content
t env var `KNOWLEDGE_INTAKE_CHANNEL_ID=<your-channel-id>` (preferred), OR edit `scripts/triage-url.js` line 19 to hardcode it.
Confidence
100% confidence
Finding
Referenced artifact was not completely inspected

Ae1

High
Category
analysis-evasion
Content
t env var `KNOWLEDGE_INTAKE_CHANNEL_ID=<your-channel-id>` (preferred), OR edit `scripts/triage-url.js` line 19 to hardcode it.
Confidence
100% confidence
Finding
Referenced artifact was not completely inspected

Ae1

High
Category
analysis-evasion
Content
t env var `KNOWLEDGE_INTAKE_CHANNEL_ID=<your-channel-id>` (preferred), OR edit `scripts/triage-url.js` line 19 to hardcode it.
Confidence
100% confidence
Finding
Referenced artifact was not completely inspected

Ae1

High
Category
analysis-evasion
Content
t env var `KNOWLEDGE_INTAKE_CHANNEL_ID=<your-channel-id>` (preferred), OR edit `scripts/triage-url.js` line 19 to hardcode it.
Confidence
100% confidence
Finding
Referenced artifact was not completely inspected

Ae1

High
Category
analysis-evasion
Content
t env var `KNOWLEDGE_INTAKE_CHANNEL_ID=<your-channel-id>` (preferred), OR edit `scripts/triage-url.js` line 19 to hardcode it.
Confidence
100% confidence
Finding
Referenced artifact was not completely inspected

Ae1

High
Category
analysis-evasion
Content
# You can also run manually: node scripts/bookmark-poll.js
Confidence
100% confidence
Finding
Referenced artifact was not completely inspected

Ae1

High
Category
analysis-evasion
Content
# You can also run manually: node scripts/bookmark-poll.js
Confidence
100% confidence
Finding
Referenced artifact was not completely inspected

Ae1

High
Category
analysis-evasion
Content
# You can also run manually: node scripts/bookmark-poll.js
Confidence
100% confidence
Finding
Referenced artifact was not completely inspected

Ae1

High
Category
analysis-evasion
Content
node scripts/backlog-sweep.js --delay 3
Confidence
100% confidence
Finding
Referenced artifact was not completely inspected

Ae1

High
Category
analysis-evasion
Content
node scripts/backlog-sweep.js --delay 3
Confidence
100% confidence
Finding
Referenced artifact was not completely inspected

Ae1

High
Category
analysis-evasion
Content
node scripts/backlog-sweep.js --delay 3
Confidence
100% confidence
Finding
Referenced artifact was not completely inspected

Credential Access

High
Category
Privilege Escalation
Content
const WORKSPACE = process.env.OPENCLAW_WORKSPACE || path.resolve(__dirname, '../../..');
const TOKEN_CACHE = path.join(WORKSPACE, 'data/x-oauth2-token-cache.json');
const SEEN_FILE = path.join(WORKSPACE, 'data/knowledge-intake-seen.json');
const SECRETS_FILE = process.env.X_OAUTH2_SECRETS_FILE || path.join(WORKSPACE, '..', '..', 'secrets', 'x-oauth2-credentials.json');

// Load refresh token from secrets file if not in env
// client_id + client_secret must come from plist env (or be passed in)
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
// Check cache — tokens last 2 hours, use if <90 min old
  const cache = loadTokenCache();
  if (cache?.access_token && (Date.now() - cache.cached_at) < 90 * 60 * 1000) {
    console.log('[bookmark-poll] Using cached access token');
    return cache.access_token;
  }
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
// Check cache — tokens last 2 hours, use if <90 min old
  const cache = loadTokenCache();
  if (cache?.access_token && (Date.now() - cache.cached_at) < 90 * 60 * 1000) {
    console.log('[bookmark-poll] Using cached access token');
    return cache.access_token;
  }
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
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"

# Load credentials from .env file if it exists (non-OpenClaw users)
ENV_FILE="${OPENCLAW_WORKSPACE:-$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)}/.env"
if [ -f "$ENV_FILE" ]; then
  set -a
Confidence
60% confidence
Finding
Code accesses credential files (SSH keys, AWS credentials, etc.). This could indicate credential theft attempts.

Credential Access

High
Category
Privilege Escalation
Content
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"

# Load credentials from .env file if it exists (non-OpenClaw users)
ENV_FILE="${OPENCLAW_WORKSPACE:-$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)}/.env"
if [ -f "$ENV_FILE" ]; then
  set -a
Confidence
60% confidence
Finding
Code accesses credential files (SSH keys, AWS credentials, etc.). This could indicate credential theft attempts.

Credential Access

High
Category
Privilege Escalation
Content
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"

# Load credentials from .env file if it exists (non-OpenClaw users)
ENV_FILE="${OPENCLAW_WORKSPACE:-$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)}/.env"
if [ -f "$ENV_FILE" ]; then
  set -a
Confidence
60% confidence
Finding
Code accesses credential files (SSH keys, AWS credentials, etc.). This could indicate credential theft attempts.

Credential Access

High
Category
Privilege Escalation
Content
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"

# Load credentials from .env file if it exists (non-OpenClaw users)
ENV_FILE="${OPENCLAW_WORKSPACE:-$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)}/.env"
if [ -f "$ENV_FILE" ]; then
  set -a
  source "$ENV_FILE"
Confidence
90% confidence
Finding
The script uses 'source' on a .env file, which executes the file as shell code rather than parsing it as data. If an attacker can modify that .env file or influence OPENCLAW_WORKSPACE to point at a malicious file, they can achieve arbitrary command execution when the scheduled wrapper runs, potentially exposing all available secrets and the user's account context.

Credential Access

High
Category
Privilege Escalation
Content
set -a
  source "$ENV_FILE"
  set +a
  echo "[run-poll] Loaded credentials from .env"
fi

# Load from OpenClaw gateway plist if present (OpenClaw users)
Confidence
60% confidence
Finding
Code accesses credential files (SSH keys, AWS credentials, etc.). This could indicate credential theft attempts.

Missing User Warnings

Medium
Confidence
94% confidence
Finding
The README prominently advertises automatic unbookmarking as part of normal operation but does not provide a strong warning that this modifies user data and may be irreversible in practice. In an automation/agent setting, this increases the risk of accidental loss of bookmark state if the skill is enabled or triggered without the user fully understanding the consequences.

Static analysis

Detected: suspicious.dangerous_exec, suspicious.exposed_secret_literal

Shell command execution detected (child_process).

Critical
Code
suspicious.dangerous_exec
Location
scripts/backlog-sweep.js:70

Shell command execution detected (child_process).

Critical
Code
suspicious.dangerous_exec
Location
scripts/bookmark-poll.js:65

Shell command execution detected (child_process).

Critical
Code
suspicious.dangerous_exec
Location
scripts/poll-channel.js:48

Shell command execution detected (child_process).

Critical
Code
suspicious.dangerous_exec
Location
scripts/triage-url.js:72

File appears to expose a hardcoded API secret or token.

Critical
Code
suspicious.exposed_secret_literal
Location
scripts/x-oauth2-authorize.js:138