Back to skill

Security audit

skroller

Security checks for vulnerabilities and agentic risk

Overview

Skroller largely does what it claims, but it needs review because exported scraped content can become local commands and the docs include bot-evasion guidance.

Review or fix the exporter before installing, especially if you will process untrusted public posts. Avoid Bear and Apple Notes export paths until command construction is made safe, do not rely on the documented --dry-run spelling, pin external CLI dependencies, and use official platform APIs where possible instead of bot-evasion techniques.

Vulnerability Patterns
  • 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
  • Remote Payload Retrieval and ExecutionFetches external code whose behavior can change after review
Findings (5)

T09 · Insecure Skill Coding Practices

Error
Location
scripts/export-to-notes.js:137
Finding
Command Injection in Bear Export Through Untrusted Post Content and Arguments<![CDATA[ ## Vulnerability Details **File Location**: `scripts/export-to-notes.js`, lines 137–170 **Vulnerability Type**: OS command injection **Risk Level**: Critical ### Vulnerable Code ```javascript content.topPosts.forEach((p, i) => { markdown += `## ${i + 1}. ${p.author || 'Unknown'}\n\n`; markdown += `${p.text || ''}\n\n`; markdown += `**Engagement:** ${extractEngagement(p)} | `; markdown += `[View](${p.url || '#'})\n\n`; markdown += `---\n\n`; }); const tagList = tags?.split(',') || ['skroller', 'research']; const tagArgs = tagList.map(t => `--tag "${t}"`).join(' '); const escaped = markdown.replace(/"/g, '\\"').replace(/\$/g, '\\$'); const command = `echo "${escaped}" | grizzly create --title "${title}" ${tagArgs}`; execSync(command, { stdio: 'inherit' }); ``` ### Technical Analysis The Bear exporter constructs a shell command by concatenating values from several untrusted sources: - Scraped post text, author names, and URLs - The generated or user-supplied title - User-supplied tags The attempted escaping only handles double quotes and dollar signs in `markdown`. It does not safely handle shell constructs such as backticks, command separators, redirections, newlines, or other shell metacharacters. The `title` and individual tag values are not escaped at all. Because `execSync()` receives a single command string, Node.js invokes a shell to interpret it. As a result, attacker-controlled social-media content can become executable shell syntax rather than remaining inert note content. ### Attack Path 1. An attacker publishes a social-media post containing a shell payload in its text, author field, or another extracted value. 2. The user runs `scripts/skroller.js` and collects the attacker-controlled post. 3. The resulting JSON is passed to `scripts/export-to-notes.js --app bear`. 4. The malicious content is inserted into `markdown`. 5. The exporter concatenates that content into the `echo ... | grizzly ...` shell command. 6. `execSync() ...[truncated 782 chars]
Remediation
<![CDATA[ ## Remediation Suggestions - Remove shell-string construction entirely. - Invoke `grizzly` with `spawnSync()` or `execFileSync()` using a fixed executable and an argument array. - Supply the Markdown document through the child process’s standard input rather than through `echo`. - Never concatenate titles, tags, URLs, authors, or post text into a shell command. - Validate tags and other identifiers against a restrictive allowlist where practical. - Treat every field loaded from scraped JSON as untrusted, even if it originated from a public platform. - Add regression tests containing quotes, backticks, newlines, redirection characters, command substitutions, and command separators. A safer design is: ```javascript const { spawnSync } = require('child_process'); const args = ['create', '--title', title]; for (const tag of tagList) { args.push('--tag', tag); } const result = spawnSync('grizzly', args, { input: markdown, encoding: 'utf8', stdio: ['pipe', 'inherit', 'inherit'], shell: false }); if (result.error) { throw result.error; } ``` ]]>

T09 · Insecure Skill Coding Practices

Error
Location
scripts/export-to-notes.js:276
Finding
Shell and AppleScript Injection in Apple Notes Export<![CDATA[ ## Vulnerability Details **File Location**: `scripts/export-to-notes.js`, lines 276–309 **Vulnerability Type**: AppleScript injection and OS command injection **Risk Level**: Critical ### Vulnerable Code ```javascript let body = `${content.title}\\n\\n`; body += `Collected: ${content.collected}\\n`; body += `Posts: ${content.postCount}\\n\\n`; body += `---\\n\\n`; content.topPosts.forEach((p, i) => { body += `${i + 1}. ${p.author || 'Unknown'}\\n`; body += `${p.text || ''}\\n`; body += `Engagement: ${extractEngagement(p)}\\n`; body += `URL: ${p.url || 'N/A'}\\n\\n`; }); const script = `tell application "Notes" activate set noteFolder to "${folder || 'Notes'}" try set targetFolder to folder noteFolder on error set targetFolder to folder "Notes" end try set newNote to make new note at targetFolder with properties {body:"${body}"} set name of newNote to "${content.title}" end tell`; try { execSync(`osascript -e '${script}'`, { stdio: 'inherit' }); console.log(`✓ Created Apple Note: "${content.title}"`); } catch (e) { throw new Error('Apple Notes export failed. Ensure Notes app is installed.'); } ``` ### Technical Analysis Untrusted values are embedded directly into an AppleScript program without AppleScript string escaping. These values include: - Scraped post text - Scraped author names - Scraped URLs - The generated title - The requested Notes folder The generated AppleScript is then embedded inside a single-quoted shell command and passed to `execSync()`. This creates two distinct injection boundaries: 1. Malicious content can terminate or alter an AppleScript string. 2. A single quote can terminate the shell argument and inject shell syntax. The use of two nested interpreters makes ad hoc escaping particularly unsafe. No escaping is applied at either boundary. ### Attack Path 1. An attacker creates a social-media post containing crafted quote characters and AppleScript or shell syntax. 2. The user colle ...[truncated 1120 chars]
Remediation
<![CDATA[ ## Remediation Suggestions - Do not embed untrusted values into executable AppleScript source. - Store the AppleScript in a static file or static source string whose program structure never depends on input. - Pass note content, title, and folder as positional arguments to `osascript`. - Invoke `osascript` with `spawnSync()` or `execFileSync()` and `shell: false`. - In the AppleScript, read values from `argv` through an `on run argv` handler. - Validate folder names if only a restricted naming format is required. - Add tests using single quotes, double quotes, backslashes, newlines, and AppleScript syntax in every imported field. For example, use a static script that accepts arguments: ```applescript on run argv set noteTitle to item 1 of argv set noteBody to item 2 of argv set noteFolder to item 3 of argv tell application "Notes" set targetFolder to folder noteFolder set newNote to make new note at targetFolder with properties {body:noteBody} set name of newNote to noteTitle end tell end run ``` Then invoke it with an argument array rather than a shell command. ]]>

T09 · Insecure Skill Coding Practices

Error
Location
scripts/export-to-notes.js:329
Finding
Unescaped Scraped Content in HTML, XHTML, and Evernote XML Exports<![CDATA[ ## Vulnerability Details **File Location**: `scripts/export-to-notes.js`, lines 329–439 **Vulnerability Type**: Stored markup injection and unsafe document generation **Risk Level**: High ### Vulnerable Code ```javascript content.topPosts.forEach((p, i) => { enex += ` <h2>${i + 1}. ${p.author || 'Unknown'}</h2>\n`; enex += ` <p>${p.text || ''}</p>\n`; enex += ` <p>Engagement: ${extractEngagement(p)}</p>\n`; enex += ` <p>URL: <a href="${p.url || '#'}">View</a></p>\n`; }); const pageContent = `<!DOCTYPE html><html><head><title>${content.title}</title></head><body> <h1>${content.title}</h1> <p><strong>Collected:</strong> ${content.collected}</p> <p><strong>Posts:</strong> ${content.postCount}</p> <hr/> ${content.topPosts.map((p, i) => ` <h2>${i + 1}. ${p.author || 'Unknown'}</h2> <p>${p.text || ''}</p> <p><strong>Engagement:</strong> ${extractEngagement(p)}</p> <p><a href="${p.url || '#'}">View post</a></p> <hr/> `).join('')} </body></html>`; const response = await fetch(endpoint, { method: 'POST', headers: { 'Authorization': `Bearer ${token}`, 'Content-Type': 'application/xhtml+xml' }, body: pageContent }); content.topPosts.forEach((p, i) => { html += `<h3>${i + 1}. ${p.author || 'Unknown'}</h3>\n`; html += `<p>${p.text || ''}</p>\n`; html += `<p><small>Engagement: ${extractEngagement(p)}</small></p>\n`; html += `<p><a href="${p.url || '#'}">View</a></p>\n`; html += `<hr/>\n`; }); fs.writeFileSync(outputPath, html); ``` ### Technical Analysis The exporter treats scraped post fields as trusted markup and directly inserts them into: - Evernote ENEX/XML - OneNote XHTML - Google Keep HTML No HTML or XML entity encoding is applied to titles, authors, post text, or URLs. In addition, URL values are inserted into `href` attributes without validating their scheme. An attacker-controlled field can therefore: - Close the intended element or attribute - Insert arbitrar ...[truncated 1955 chars]
Remediation
<![CDATA[ ## Remediation Suggestions - Apply context-specific HTML or XML encoding to every untrusted text value. - Encode attribute values separately from element text. - Use a maintained XML or DOM construction library instead of string concatenation. - Validate URLs with the `URL` class and allow only expected schemes such as `https:` and, where required, `http:`. - Reject dangerous schemes including `javascript:`, `data:`, and `file:` unless explicitly necessary. - Avoid hand-constructed CDATA sections. If CDATA is required, safely split or encode content that contains the CDATA terminator. - Sanitize output according to each destination application’s documented accepted markup. - Add tests for angle brackets, ampersands, quotes, malformed tags, attribute breakouts, dangerous URL schemes, and CDATA termination attempts. Example text encoding should convert at least: ```text & -> &amp; < -> &lt; > -> &gt; " -> &quot; ' -> &#39; ``` ]]>

T09 · Insecure Skill Coding Practices

Error
Location
scripts/export-to-notes.js:30
Finding
Documented Dry-Run Safeguard Is Not Recognized by the Argument Parser<![CDATA[ ## Vulnerability Details **File Location**: `scripts/export-to-notes.js`, lines 30–48 and 595–607; `SKILL.md`, line 129 **Vulnerability Type**: Safety-control bypass caused by inconsistent option naming **Risk Level**: High ### Vulnerable Code The parser recognizes only the camel-case key `dryRun`: ```javascript function parseArgs(args) { const parsed = {}; for (let i = 0; i < args.length; i++) { if (args[i].startsWith('--')) { const key = args[i].slice(2); const value = args[i + 1]; if (key === 'limit') { parsed[key] = parseInt(value || '10'); } else if (key === 'dryRun') { parsed[key] = value === 'true' || value === undefined; } else { parsed[key] = value; } i++; } } return parsed; } ``` The execution path checks the same camel-case property: ```javascript const dryRun = args.dryRun === true; const options = { dryRun, title: args.title || content.title, tags: args.tags, vault: args.vault || config.export?.vault, folder: args.folder || config.export?.folder, filename: args.filename, apiKey: args.apiKey || process.env.NOTION_API_KEY, databaseId: args.databaseId || config.export?.notionDatabaseId, accessToken: args.accessToken || process.env.MS_GRAPH_TOKEN, output: args.output, limit: args.limit }; ``` However, the documented command uses kebab case: ```bash node scripts/export-to-notes.js --input posts.json --app obsidian --dry-run ``` ### Technical Analysis `parseArgs()` removes the leading dashes but performs no normalization. Consequently, `--dry-run` creates `args['dry-run']`, while the exporter checks `args.dryRun`. The documented command therefore leaves `dryRun` set to `false`. Export functions proceed with real filesystem writes, child-process execution, or network requests instead of preview-only behavior. The generic parser also assumes every option consumes the next argument. This behavior is unsuitable for boolean flags and ca ...[truncated 1142 chars]
Remediation
<![CDATA[ ## Remediation Suggestions - Recognize the documented `--dry-run` spelling. - Normalize kebab-case option names to a canonical internal representation. - Treat boolean options separately and do not consume the following argument unless it is an explicit boolean value. - Consider supporting both `--dry-run` and `--dryRun` temporarily for compatibility. - Make dry-run enforcement centralized so exporters cannot accidentally omit it. - Add integration tests asserting that dry-run performs: - No filesystem writes - No child-process execution - No AppleScript execution - No network requests - Update help output and documentation so accepted option names are generated from the same option definition. For example: ```javascript if (key === 'dry-run' || key === 'dryRun') { parsed.dryRun = true; continue; } ``` A mature command-line parser library can also reduce inconsistent flag handling. ]]>

T08 · Insecure Dependencies

Warning
Location
scripts/export-to-notes.js:158
Finding
Mutable Third-Party CLI Installation Instruction Uses an Unpinned Latest Version<![CDATA[ ## Vulnerability Details **File Location**: `scripts/export-to-notes.js`, lines 158–163; `SKILL.md`, line 143 **Vulnerability Type**: Unpinned third-party dependency and supply-chain exposure **Risk Level**: Medium ### Vulnerable Code ```javascript try { execSync('which grizzly', { stdio: 'ignore' }); } catch (e) { throw new Error( 'grizzly CLI not found. Install: go install github.com/tylerwince/grizzly/cmd/grizzly@latest' ); } ``` The same mutable installation instruction is included in the documentation: ```text go install github.com/tylerwince/grizzly/cmd/grizzly@latest ``` ### Technical Analysis The Skill instructs users to download, compile, and install the latest available revision of a third-party command-line program. The `latest` reference is mutable and can resolve to code that differs from what was available when this Skill was audited. No reviewed version, commit identifier, checksum, or signature-verification procedure is provided. If the upstream project, release process, maintainer account, or dependency chain is compromised, following this instruction can introduce changed or malicious code onto the local system. The Skill does not automatically run the installation command, so user action is required. However, the instruction appears in both the runtime error and official Skill documentation, making it part of the intended setup flow. ### Attack Path 1. The upstream project or one of its dependencies is compromised, or an unexpected unsafe release becomes the latest version. 2. A user attempts a Bear export without `grizzly` installed. 3. The Skill instructs the user to run the unpinned `go install ...@latest` command. 4. Go retrieves and builds the current upstream source and dependency graph. 5. The resulting executable is installed into the user’s environment. 6. The Skill later executes that binary during Bear export. 7. A compromised binary can access the exported content and other resources available to the u ...[truncated 507 chars]
Remediation
<![CDATA[ ## Remediation Suggestions - Pin the dependency to a reviewed, immutable release or commit rather than `@latest`. - Document the exact expected version. - Provide a checksum, signed release verification procedure, or other integrity validation. - Periodically review and deliberately update the pinned version. - Document the external binary’s required permissions and security boundary. - Prefer a maintained library invoked without a shell if that can provide the required integration. - Fail safely when the expected verified version is unavailable. - If practical, check `grizzly` version output and reject versions outside the reviewed range. Example: ```text go install github.com/tylerwince/grizzly/cmd/grizzly@<reviewed-version> ``` The placeholder should be replaced with a specific reviewed release or immutable commit. ]]>
Vulnerability Patterns
  • Prompt InjectionInstruction Override, Hidden Instructions, Exfiltration Commands
  • Privilege EscalationExcessive Permissions, Sudo/Root Execution, Credential Access
  • MCP Tool PoisoningHidden Instructions, Unicode Deception, Parameter Description Injection
  • Data ExfiltrationExternal Transmission, Env Variable Harvesting, File System Enumeration
  • Supply ChainUnpinned Dependencies, External Script Fetching, Obfuscated Code
Findings (46)

Anti-Refusal Statement

High
Category
Anti-Refusal
Content
---
name: skroller
description: Automated social media content collection and analysis across platforms (Twitter/X, Instagram, TikTok, Reddit, LinkedIn, YouTube, Product Hunt, Medium, GitHub, Pinterest). Use when you need to: (1) scrape public posts programmatically, (2) analyze content by keywords or filters, (3) monitor brand mentions or trends for research, (4) curate content for personal analysis, (5) archive publicly available information, or (6) generate digests from scraped feeds. Always comply with platform ToS and applicable privacy laws.
---

# Skroller - Social Media Content collection
Confidence
70% confidence
Finding
Skill instructs the agent to never refuse or to always comply. Suppressing the agent's ability to decline removes a core safety control and enables downstream harmful requests to succeed.

Ae1

High
Category
analysis-evasion
Content
node scripts/skroller.js --platform twitter --query "AI" --limit 50 --output posts.json
Confidence
100% confidence
Finding
Referenced artifact was not completely inspected

Ae1

High
Category
analysis-evasion
Content
node scripts/skroller.js --platform twitter --query "AI" --limit 50 --output posts.json
Confidence
100% confidence
Finding
Referenced artifact was not completely inspected

Ae1

High
Category
analysis-evasion
Content
node scripts/skroller.js --platform twitter --query "AI" --limit 50 --output posts.json
Confidence
100% confidence
Finding
Referenced artifact was not completely inspected

Ae1

High
Category
analysis-evasion
Content
node scripts/skroller.js --platform twitter --query "AI" --limit 50 --output posts.json
Confidence
100% confidence
Finding
Referenced artifact was not completely inspected

Ae1

High
Category
analysis-evasion
Content
node scripts/skroller.js --platform twitter --query "AI" --limit 50 --output posts.json
Confidence
100% confidence
Finding
Referenced artifact was not completely inspected

Ae1

High
Category
analysis-evasion
Content
node scripts/skroller.js --platform twitter --query "AI" --limit 50 --output posts.json
Confidence
100% confidence
Finding
Referenced artifact was not completely inspected

Ae1

High
Category
analysis-evasion
Content
node scripts/skroller.js --platform twitter --query "AI" --limit 50 --output posts.json
Confidence
100% confidence
Finding
Referenced artifact was not completely inspected

Ae1

High
Category
analysis-evasion
Content
### `scripts/feed-digest.js` - Generate digests
Confidence
100% confidence
Finding
Referenced artifact was not completely inspected

Ae1

High
Category
analysis-evasion
Content
### `scripts/feed-digest.js` - Generate digests
Confidence
100% confidence
Finding
Referenced artifact was not completely inspected

Ae1

High
Category
analysis-evasion
Content
### `scripts/export-to-notes.js` - Unified note app exporter
Confidence
100% confidence
Finding
Referenced artifact was not completely inspected

Ae1

High
Category
analysis-evasion
Content
### `scripts/export-to-notes.js` - Unified note app exporter
Confidence
100% confidence
Finding
Referenced artifact was not completely inspected

Ae1

High
Category
analysis-evasion
Content
### `scripts/export-to-notes.js` - Unified note app exporter
Confidence
100% confidence
Finding
Referenced artifact was not completely inspected

Ae1

High
Category
analysis-evasion
Content
### `scripts/export-to-notes.js` - Unified note app exporter
Confidence
100% confidence
Finding
Referenced artifact was not completely inspected

Ae1

High
Category
analysis-evasion
Content
### `scripts/export-to-notes.js` - Unified note app exporter
Confidence
100% confidence
Finding
Referenced artifact was not completely inspected

Ae1

High
Category
analysis-evasion
Content
### `scripts/export-to-notes.js` - Unified note app exporter
Confidence
100% confidence
Finding
Referenced artifact was not completely inspected

Ae1

High
Category
analysis-evasion
Content
### `scripts/export-to-notes.js` - Unified note app exporter
Confidence
100% confidence
Finding
Referenced artifact was not completely inspected

Ae1

High
Category
analysis-evasion
Content
### `scripts/export-to-notes.js` - Unified note app exporter
Confidence
100% confidence
Finding
Referenced artifact was not completely inspected

Ae1

High
Category
analysis-evasion
Content
### `scripts/export-to-notes.js` - Unified note app exporter
Confidence
100% confidence
Finding
Referenced artifact was not completely inspected

Ae1

High
Category
analysis-evasion
Content
### `scripts/export-to-notes.js` - Unified note app exporter
Confidence
100% confidence
Finding
Referenced artifact was not completely inspected

Ae1

High
Category
analysis-evasion
Content
### `scripts/export-to-notes.js` - Unified note app exporter
Confidence
100% confidence
Finding
Referenced artifact was not completely inspected

Ae1

High
Category
analysis-evasion
Content
### `scripts/export-to-notes.js` - Unified note app exporter
Confidence
100% confidence
Finding
Referenced artifact was not completely inspected

Ae1

High
Category
analysis-evasion
Content
### `scripts/export-to-notes.js` - Unified note app exporter
Confidence
100% confidence
Finding
Referenced artifact was not completely inspected

Credential Access

High
Category
Privilege Escalation
Content
- **Notion:** API key (create at notion.so)
- **Apple Notes:** macOS with Notes app
- **Evernote:** Manual ENEX import
- **OneNote:** Microsoft Graph access token
- **Google Keep:** Manual HTML import
- **Roam Research:** Markdown import (drag MDL file into Roam)
- **Logseq:** Vault path (writes to pages/ folder)
Confidence
70% confidence
Finding
Code accesses credential files (SSH keys, AWS credentials, etc.). This could indicate credential theft attempts.

Intent-Code Divergence

High
Confidence
97% confidence
Finding
The document explicitly says not to hide automated activity, but later provides anti-bot avoidance guidance such as user-agent rotation, random delays, mouse simulation, and proxy rotation. That contradiction undermines the stated safety framing and makes the content operationally useful for evading platform detection rather than merely respecting rate limits.

Static analysis

Detected: suspicious.dangerous_exec, suspicious.dynamic_code_execution, suspicious.env_credential_access (+1 more)

Shell command execution detected (child_process).

Critical
Code
suspicious.dangerous_exec
Location
scripts/export-to-notes.js:160

Dynamic code execution detected.

Critical
Code
suspicious.dynamic_code_execution
Location
scripts/skroller.js:28

Environment variable access combined with network send.

Critical
Code
suspicious.env_credential_access
Location
scripts/export-to-notes.js:232

File appears to expose a hardcoded API secret or token.

Critical
Code
suspicious.exposed_secret_literal
Location
scripts/export-to-notes.js:620