Back to skill

Security audit

Twitter Article

Security checks for vulnerabilities and agentic risk

Overview

This skill appears purpose-built for syncing Notion content to Twitter/X Articles, but it handles powerful account credentials and remote publishing/deletion with insufficient safeguards.

Review before installing. Use only a dedicated Twitter/X account and a least-privilege Notion integration, avoid pasting secrets into shared shells or scripts, rotate any exposed tokens, use only a trusted local proxy, and treat publish/delete commands as immediate account actions.

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

Warning
Location
twitter-article.js:514
Finding
Notion integration token exposed through command-line arguments<![CDATA[ ## Vulnerability Details **File Location**: `SKILL.md:12`; `twitter-article.js:514-526` **Vulnerability Type**: Sensitive credential exposure through process arguments **Risk Level**: Medium ### Vulnerable Code ```bash # SKILL.md:12 node twitter-article.js notion-to-article --notion-key <key> --page-id <id> ``` ```javascript // twitter-article.js:514-526 async function main() { const args = process.argv.slice(2); const cmd = args[0]; const getArg = (name) => { const i = args.indexOf(`--${name}`); return i >= 0 ? args[i + 1] : null; }; try { switch (cmd) { // ... case 'notion-to-article': await cmdNotionToArticle(getArg('notion-key'), getArg('page-id'), { publish: args.includes('--publish') }); break; ``` ### Technical Analysis The documented interface instructs users to supply the Notion integration secret through the `--notion-key` command-line option. The implementation then retrieves that secret directly from `process.argv`. Command-line arguments are not an appropriate channel for secrets because they may be exposed through: - Shell history files. - Process listings and process-monitoring utilities. - Audit, telemetry, debugging, or orchestration logs that record command lines. - Wrapper scripts, job definitions, or CI logs containing the full invocation. - Error reports or administrative tooling that captures process metadata. The Notion token is legitimately required for the declared Notion-to-Twitter synchronization operation, and the code sends it only to the official Notion API. The vulnerability is therefore not unauthorized network exfiltration; it is insecure local credential handling that unnecessarily broadens access to the token. ### Attack Path 1. A user follows the documented example and runs the Skill with `--notion-key <secret>`. 2. The secret becomes part of the process command line and may also be written to shell history. 3. A local user, process-monitoring service, CI logger, o ...[truncated 821 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. Remove support for passing the Notion token directly through `--notion-key`, or retain it only with a prominent deprecation warning. 2. Read the token from a dedicated environment variable, such as `NOTION_TOKEN`: ```javascript const notionKey = process.env.NOTION_TOKEN; if (!notionKey) { throw new Error('NOTION_TOKEN is required'); } ``` 3. For interactive use, support a hidden terminal prompt that does not echo the token. 4. In automated environments, retrieve the token from the platform's secret manager rather than storing it in scripts or job arguments. 5. Update `SKILL.md` to recommend secure secret injection: ```bash export NOTION_TOKEN="<notion integration token>" node twitter-article.js notion-to-article --page-id <id> ``` 6. Advise existing users to remove exposed commands from shell history and rotate any token that may have entered logs. 7. Ensure error messages and diagnostic output never print the token or complete authorization headers. ]]>

T09 · Insecure Skill Coding Practices

Warning
Location
twitter-article.js:455
Finding
Predictable shared temporary paths permit content disclosure and symlink-based file overwrite<![CDATA[ ## Vulnerability Details **File Location**: `twitter-article.js:455-481` **Vulnerability Type**: Unsafe temporary-file creation and sensitive-data persistence **Risk Level**: Medium ### Vulnerable Code ```javascript // twitter-article.js:455-481 const mediaIds = []; const tmpDir = '/tmp/twitter-article-imgs'; fs.mkdirSync(tmpDir, { recursive: true }); for (let i = 0; i < images.length; i++) { const imgUrl = images[i]; const ext = imgUrl.match(/\.(png|jpg|jpeg|gif|webp)/i)?.[1] || 'jpg'; const localPath = `${tmpDir}/img${i}.${ext}`; console.log(` 📤 [${i + 1}/${images.length}] Downloading & uploading...`); try { await downloadImage(imgUrl, localPath); const upload = await uploadMedia(localPath); mediaIds.push(upload.media_id); } catch (e) { console.error(` ⚠️ Image ${i + 1} failed: ${e.message}`); } } console.log(` Uploaded: ${mediaIds.length}/${images.length}`); // 5. Convert to Twitter format const content = markdownToTwitterContent(markdown, mediaIds); const contentFile = '/tmp/twitter-article-content.json'; fs.writeFileSync(contentFile, JSON.stringify(content)); ``` The image downloader writes to these paths without exclusive creation or symlink protection: ```javascript response.on('end', () => { fs.writeFileSync(destPath, Buffer.concat(chunks)); resolve(destPath); }); ``` ### Technical Analysis The Skill uses globally predictable paths in the shared `/tmp` directory: - `/tmp/twitter-article-imgs` - `/tmp/twitter-article-imgs/img0.jpg` and similar deterministic names - `/tmp/twitter-article-content.json` Files are written using `fs.writeFileSync` without: - A per-execution random temporary directory. - Exclusive file creation. - Explicit restrictive permissions. - Symlink checks. - Ownership validation. - Cleanup after completion. `fs.writeFileSync` follows symbolic links. An attacker who can pre-create one of the predictable paths as a symbolic link may redirect the write to another file writab ...[truncated 2254 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. Create a unique private directory for every execution with `fs.mkdtempSync` and `os.tmpdir()`: ```javascript const os = require('os'); const path = require('path'); const tmpDir = fs.mkdtempSync( path.join(os.tmpdir(), 'twitter-article-'), { encoding: 'utf8' } ); fs.chmodSync(tmpDir, 0o700); ``` 2. Store both images and converted article content inside that private directory. 3. Open files with restrictive permissions and exclusive creation where applicable: ```javascript fs.writeFileSync(contentFile, JSON.stringify(content), { mode: 0o600, flag: 'wx' }); ``` 4. Avoid following attacker-controlled symbolic links. Use exclusive creation, verify paths with `lstat`, and ensure every destination remains inside the newly created temporary directory. 5. Remove all temporary content in a `finally` block: ```javascript try { // Download, convert, and upload content. } finally { fs.rmSync(tmpDir, { recursive: true, force: true }); } ``` 6. Use unique file names rather than deterministic names such as `img0.jpg`. 7. Do not reuse a global content file. Pass the generated content object directly to draft creation, or create the JSON file only inside the private per-run directory. 8. Document that temporary files may contain confidential unpublished content and should never be retained longer than required. ]]>
Vulnerability Patterns
  • Data ExfiltrationExternal Transmission, Env Variable Harvesting, File System Enumeration
  • MCP Tool PoisoningHidden Instructions, Unicode Deception, Parameter Description Injection
  • Prompt InjectionInstruction Override, Hidden Instructions, Exfiltration Commands
  • Privilege EscalationExcessive Permissions, Sudo/Root Execution, Credential Access
  • Supply ChainUnpinned Dependencies, External Script Fetching, Obfuscated Code
Findings (7)

Missing User Warnings

Medium
Confidence
96% confidence
Finding
The skill instructs users to export live Twitter/X authentication cookies (`AUTH_TOKEN` and `CT0`) from their browser and use them directly, but it does not warn that these are highly sensitive session credentials equivalent to account access. In a CLI/integration context, users may paste them into shells, logs, shared terminals, or scripts, creating a realistic risk of session theft and unauthorized posting or account takeover actions.

Intent-Code Divergence

Medium
Confidence
95% confidence
Finding
The top-level doc comment presents this file as a Twitter/X Article CLI for creating and managing articles via Twitter GraphQL, listing only Twitter-focused commands. However, the code also implements an end-to-end `notion-to-article` workflow that fetches content from the Notion API, downloads remote images, writes temporary files, uploads media to Twitter, and can publish automatically, which is a materially broader behavior than the documentation describes.

Missing User Warnings

Medium
Confidence
96% confidence
Finding
The script sends Twitter auth cookies and CSRF token through a user-configurable proxy, and even defaults to a local HTTP proxy. That means a malicious or misconfigured proxy can observe or intercept highly sensitive session credentials, enabling account takeover or unauthorized actions on the user's X/Twitter account; the Notion flow also broadens data exposure by moving third-party content through the same network path.

Missing User Warnings

Medium
Confidence
96% confidence
Finding
cmdDelete directly invokes the ArticleEntityDelete API and only reports success or failure afterward. There is no confirmation prompt, dry-run option, or cautionary message despite this being a destructive operation against the user's remote article data.

External Transmission

Medium
Category
Data Exfiltration
Content
const blocks = [];
  let cursor;
  do {
    const url = `https://api.notion.com/v1/blocks/${blockId}/children?page_size=100${cursor ? '&start_cursor=' + cursor : ''}`;
    const res = await fetch(url, {
      headers: { 'Authorization': `Bearer ${notionKey}`, 'Notion-Version': '2022-06-28' },
    });
Confidence
60% confidence
Finding
Data is being sent to an external URL. This could be legitimate telemetry or data exfiltration. Manual review is recommended.

External Transmission

Medium
Category
Data Exfiltration
Content
const blocks = [];
  let cursor;
  do {
    const url = `https://api.notion.com/v1/blocks/${blockId}/children?page_size=100${cursor ? '&start_cursor=' + cursor : ''}`;
    const res = await fetch(url, {
      headers: { 'Authorization': `Bearer ${notionKey}`, 'Notion-Version': '2022-06-28' },
    });
Confidence
60% confidence
Finding
Data is being sent to an external URL. This could be legitimate telemetry or data exfiltration. Manual review is recommended.

Natural-Language Policy Violations

Low
Confidence
94% confidence
Finding
The request headers hard-code x-twitter-client-language to 'en', which enforces a specific locale regardless of user preference. The file does not offer an opt-in or configuration mechanism, and no region-specific justification is documented.