Back to skill

Security audit

alista

Security checks for vulnerabilities and agentic risk

Overview

The skill is a coherent local bookmark tool, but its social-media fetcher can be tricked into contacting non-social or local network URLs and can write downloaded media outside the intended workspace.

Review before installing. Use this only if you are comfortable sending social-media URLs, captions, tagged users, transcripts/media metadata, and place queries to Apify and Google Places. Until the URL validation issues are fixed, avoid processing untrusted links and avoid --download-images/--extract-frames outside a tightly network-restricted workspace.

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 (3)

T09 · Insecure Skill Coding Practices

Error
Location
scripts/fetch-post.ts:110
Finding
Server-Side Request Forgery Through Insufficient Social Platform URL Validation<![CDATA[ ## Vulnerability Details **File Location**: `scripts/fetch-post.ts:110-124` and `scripts/lib/metadata-fetcher.ts:191-221` **Vulnerability Type**: Server-Side Request Forgery (SSRF) **Risk Level**: High ### Vulnerable Code ```ts // scripts/fetch-post.ts:110-124 // Detect platform const isInstagram = validUrl.includes("instagram.com"); const isTikTok = validUrl.includes("tiktok.com") || validUrl.includes("vm.tiktok.com"); if (!isInstagram && !isTikTok) { console.error(JSON.stringify({ error: "Unsupported platform. Supports Instagram and TikTok." })); process.exit(1); } const startTime = Date.now(); const metadata = isInstagram ? await fetcher.getInstagramPost(validUrl) : await fetcher.getTiktokVideo(validUrl); ``` ```ts // scripts/lib/metadata-fetcher.ts:191-221 private async getInstagramPostFromOgTags(url: string): Promise<PostMetadata | null> { try { const resp = await fetch(url, { headers: { "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36", Accept: "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8", "Accept-Language": "en-US,en;q=0.9", "Accept-Encoding": "gzip, deflate, br", "Cache-Control": "no-cache", Pragma: "no-cache", "Sec-Fetch-Dest": "document", "Sec-Fetch-Mode": "navigate", "Sec-Fetch-Site": "none", "Sec-Fetch-User": "?1", "Upgrade-Insecure-Requests": "1", }, signal: AbortSignal.timeout(TIMEOUTS.ogTags), redirect: "follow", }); if (!resp.ok) { console.error("[MetadataFetcher] Failed to fetch Instagram page:", resp.status); return null; } const html = await resp.text(); const ogTags = extractOgTags(html); ``` ### Technical Analysis The platform check uses substring matching rather than parsing and validating the URL hostname. Any URL containing the text `instagram.com` or `tiktok.com` can pass, even when its actua ...[truncated 2424 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. Parse input with `new URL()` before platform classification. 2. Require the `https:` protocol. 3. Compare normalized hostnames rather than using substring matching: - Permit `instagram.com` and hostnames ending in `.instagram.com`. - Permit `tiktok.com` and hostnames ending in `.tiktok.com`. 4. Reject URLs containing embedded credentials or nonstandard ports unless explicitly required. 5. Resolve the hostname before connecting and reject: - Loopback addresses. - RFC 1918 private addresses. - Link-local addresses. - Carrier-grade NAT ranges. - Multicast, unspecified, and other reserved addresses. - IPv4-mapped IPv6 variants of prohibited addresses. 6. Disable automatic redirects or validate the scheme, hostname, and resolved address at every redirect hop. 7. Apply the same centralized validator to Apify inputs, direct Open Graph requests, TikTok oEmbed inputs, and media URLs. 8. Add regression tests for deceptive hostnames, user-info syntax, encoded IP addresses, IPv6 addresses, DNS rebinding, and redirects to private networks. ]]>

T09 · Insecure Skill Coding Practices

Warning
Location
scripts/fetch-post.ts:43
Finding
Unrestricted Metadata-Supplied Image Downloads Permit SSRF<![CDATA[ ## Vulnerability Details **File Location**: `scripts/fetch-post.ts:43-49` and `scripts/fetch-post.ts:145-157` **Vulnerability Type**: Server-Side Request Forgery Through Unvalidated Media URLs **Risk Level**: Medium ### Vulnerable Code ```ts // scripts/fetch-post.ts:43-49 async function downloadImage(imageUrl: string, outPath: string): Promise<boolean> { try { const resp = await fetch(imageUrl, { signal: AbortSignal.timeout(15000) }); if (!resp.ok) return false; const buffer = Buffer.from(await resp.arrayBuffer()); await writeFile(outPath, buffer); return true; } catch { return false; } } ``` ```ts // scripts/fetch-post.ts:145-157 if (downloadImagesDir && metadata.imageUrls && metadata.imageUrls.length > 0) { const absDir = resolve(downloadImagesDir); const cwd = process.cwd(); if (!absDir.startsWith(cwd)) { console.error(JSON.stringify({ error: "Download directory must be under the current working directory" })); process.exit(1); } await mkdir(absDir, { recursive: true }); const downloadedPaths: string[] = []; for (let i = 0; i < metadata.imageUrls.length; i++) { const ext = "jpg"; const outPath = join(absDir, `image_${i + 1}.${ext}`); const ok = await downloadImage(metadata.imageUrls[i], outPath); if (ok) downloadedPaths.push(outPath); } ``` ### Technical Analysis Image URLs returned by Apify or extracted from Instagram Open Graph metadata are treated as trusted and passed directly to `fetch()`. Unlike video frame extraction, which calls `isSafeMediaUrl()`, the image downloader does not enforce: - HTTPS. - An approved CDN hostname. - Publicly routable destination addresses. - Redirect destination validation. - Response content type. - Maximum response size. Metadata obtained from a remote page or third-party scraper is untrusted. A malicious social page, manipulated scraper result, or compromised upstream service can therefore provide an image URL targeting localhost or a private service. The unrestrict ...[truncated 1367 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. Validate every image URL with the same strict media URL policy used for videos. 2. Require HTTPS and allow only exact approved CDN domains or their legitimate subdomains. 3. Resolve hostnames and reject private, loopback, link-local, and reserved IP ranges. 4. Disable redirects or validate every redirect destination before following it. 5. Require an expected image MIME type such as `image/jpeg`, `image/png`, or `image/webp`. 6. Stream responses to disk rather than loading the complete body into memory. 7. Enforce a strict maximum download size using both `Content-Length` and a streaming byte counter. 8. Limit the number of images downloaded from a single post. 9. Delete partial files if validation fails or a download exceeds its limit. 10. Treat all URLs returned by Apify and social-media pages as untrusted, regardless of the apparent source. ]]>

T09 · Insecure Skill Coding Practices

Warning
Location
scripts/fetch-post.ts:143
Finding
Output Directory Containment Check Can Be Bypassed by Prefix-Collision Paths<![CDATA[ ## Vulnerability Details **File Location**: `scripts/fetch-post.ts:143-173` **Vulnerability Type**: Improper Path Validation and Predictable File Overwrite **Risk Level**: Medium ### Vulnerable Code ```ts // scripts/fetch-post.ts:143-173 // Download images if requested if (downloadImagesDir && metadata.imageUrls && metadata.imageUrls.length > 0) { const absDir = resolve(downloadImagesDir); const cwd = process.cwd(); if (!absDir.startsWith(cwd)) { console.error(JSON.stringify({ error: "Download directory must be under the current working directory" })); process.exit(1); } await mkdir(absDir, { recursive: true }); const downloadedPaths: string[] = []; for (let i = 0; i < metadata.imageUrls.length; i++) { const ext = "jpg"; const outPath = join(absDir, `image_${i + 1}.${ext}`); const ok = await downloadImage(metadata.imageUrls[i], outPath); if (ok) downloadedPaths.push(outPath); } result.downloadedImages = downloadedPaths; } // Extract video frames if requested if (extractFramesDir && metadata.videoUrl) { const absDir = resolve(extractFramesDir); const cwd = process.cwd(); if (!absDir.startsWith(cwd)) { console.error(JSON.stringify({ error: "Frames directory must be under the current working directory" })); process.exit(1); } await mkdir(absDir, { recursive: true }); const frames = await extractVideoFrames(metadata.videoUrl, absDir); result.extractedFrames = frames; } ``` ### Technical Analysis The implementation attempts to restrict output directories to the current working directory using: ```ts absDir.startsWith(cwd) ``` String-prefix matching does not establish filesystem containment. For example, when the working directory is `/tmp/app`, the path `/tmp/app-backup` starts with `/tmp/app` but is not inside it. Both image downloads and extracted video frames use the same flawed check. The generated filenames are predictable: ```text image_1.jpg frame_1.jpg ``` `writeFile()` overwrites existing files by defa ...[truncated 1856 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. Replace string-prefix validation with path-aware containment checking: ```ts import { isAbsolute, relative } from "node:path"; const relativePath = relative(cwd, absDir); if ( relativePath === ".." || relativePath.startsWith(`..${path.sep}`) || isAbsolute(relativePath) ) { throw new Error("Output directory must be inside the current working directory"); } ``` 2. Compare canonical paths using `realpath()` after creating the parent directory. 3. Reject symbolic-link components or open files using operating-system options that prevent symlink following where supported. 4. Use exclusive file creation (`wx`) for image files unless explicit overwrite behavior is requested. 5. Remove ffmpeg’s `-y` option or replace it with non-overwriting behavior. 6. Prefer a Skill-controlled output root rather than accepting arbitrary output directories. 7. Create a unique per-operation subdirectory with restrictive permissions. 8. Verify the final parent directory immediately before each write to reduce time-of-check/time-of-use risks. ]]>
Vulnerability Patterns
  • Data ExfiltrationExternal Transmission, Env Variable Harvesting, File System Enumeration
  • Supply ChainUnpinned Dependencies, External Script Fetching, Obfuscated Code
  • Tool MisuseTool Parameter Abuse, Chaining Abuse, Unsafe Defaults
  • MCP Least PrivilegeUnderdeclared Capability, Wildcard Permission, Missing Permission Declaration
  • MCP Tool PoisoningHidden Instructions, Unicode Deception, Parameter Description Injection
Findings (30)

Tp4

High
Category
MCP Tool Poisoning
Confidence
95% confidence
Finding
The skill collects broader social-media metadata such as captions, transcripts, tagged users, and possibly profile/business information beyond the narrowly stated purpose of saving places. This data overcollection can expose personal or third-party information and creates a mismatch between user expectations and actual processing. In a social-media scraping context, that mismatch is security-relevant because unneeded data collection increases privacy and compliance risk.

Tp4

High
Category
MCP Tool Poisoning
Confidence
96% confidence
Finding
The skill collects broader social-media metadata such as captions, transcripts, tagged users, and possibly profile/business information beyond the narrowly stated purpose of saving places. This data overcollection can expose personal or third-party information and creates a mismatch between user expectations and actual processing. In a social-media scraping context, that mismatch is security-relevant because unneeded data collection increases privacy and compliance risk.

Tp4

High
Category
MCP Tool Poisoning
Confidence
99% confidence
Finding
The skill collects broader social-media metadata such as captions, transcripts, tagged users, and possibly profile/business information beyond the narrowly stated purpose of saving places. This data overcollection can expose personal or third-party information and creates a mismatch between user expectations and actual processing. In a social-media scraping context, that mismatch is security-relevant because unneeded data collection increases privacy and compliance risk.

Tp4

High
Category
MCP Tool Poisoning
Confidence
95% confidence
Finding
The skill collects broader social-media metadata such as captions, transcripts, tagged users, and possibly profile/business information beyond the narrowly stated purpose of saving places. This data overcollection can expose personal or third-party information and creates a mismatch between user expectations and actual processing. In a social-media scraping context, that mismatch is security-relevant because unneeded data collection increases privacy and compliance risk.

Tp4

High
Category
MCP Tool Poisoning
Confidence
91% confidence
Finding
The skill collects broader social-media metadata such as captions, transcripts, tagged users, and possibly profile/business information beyond the narrowly stated purpose of saving places. This data overcollection can expose personal or third-party information and creates a mismatch between user expectations and actual processing. In a social-media scraping context, that mismatch is security-relevant because unneeded data collection increases privacy and compliance risk.

Ae1

High
Category
analysis-evasion
Content
tsx scripts/fetch-post.ts "<url>"
Confidence
100% confidence
Finding
Referenced artifact was not completely inspected

Ae1

High
Category
analysis-evasion
Content
tsx scripts/fetch-post.ts "<url>"
Confidence
100% confidence
Finding
Referenced artifact was not completely inspected

Missing User Warnings

Medium
Confidence
95% confidence
Finding
The README encourages fetching Instagram/TikTok metadata and optionally downloading images or extracting video frames, while also requiring third-party API services such as Apify and Google Places. Without an explicit privacy warning, users may not realize that post URLs, metadata, media-derived content, and place queries can be transmitted to external providers, creating a meaningful transparency and data-handling risk.

Lp3

Medium
Category
MCP Least Privilege
Confidence
94% confidence
Finding
The manifest declares environment variables and network access but does not define an explicit tool/permission scope. That creates an overbroad execution model where an agent may use capabilities not clearly bounded by policy, increasing the chance of unintended external calls or secret use. In a skill that processes untrusted social-media URLs and calls third-party APIs, this ambiguity is more dangerous than in a purely local utility.

Description-Behavior Mismatch

Medium
Confidence
98% confidence
Finding
The manifest describes a skill for saving restaurants, bars, and cafes from TikTok and Instagram videos, but the code explicitly models an additional content type of "event". That event support is not just incidental typing: it is carried through save and update paths, indicating behavior beyond the described skill scope.

Description-Behavior Mismatch

Medium
Confidence
89% confidence
Finding
The manifest frames the skill as one that saves places, searches saved places, and provides weekend suggestions. Exposing a delete capability materially changes the skill from a save/search assistant into one that can permanently remove stored user data, which is not disclosed in the description.

Missing User Warnings

Medium
Confidence
92% confidence
Finding
The deletePlace function permanently deletes rows from the places table, which is a destructive operation. In this file there is no confirmation prompt, logging, print statement, or explanatory comment/docstring warning that the action is irreversible.

External Transmission

Medium
Category
Data Exfiltration
Content
try {
			const actorId = "apify~instagram-scraper";
			const runUrl = `https://api.apify.com/v2/acts/${actorId}/run-sync-get-dataset-items?token=${this.apifyApiKey}`;

			const resp = await fetch(runUrl, {
				method: "POST",
Confidence
92% confidence
Finding
This outbound request transmits an Instagram profile URL containing the target username to Apify, which is an external service. In this skill, users are saving and analyzing social-media-derived places, so sharing those looked-up accounts with a third party can expose user interests and activity patterns, making the transmission materially privacy-relevant rather than merely incidental network use.

Missing User Warnings

Medium
Confidence
94% confidence
Finding
The code sends user-derived Instagram usernames and profile/post URLs to Apify, a third-party scraping service, without any indication in this component that users are informed or that data sharing is minimized. This creates a real privacy and compliance risk because saved social media links and identifiers may be disclosed to an external processor unexpectedly, especially given the skill’s purpose of collecting users’ saved places from social platforms.

External Transmission

Medium
Category
Data Exfiltration
Content
private async getInstagramPostFromApify(url: string): Promise<PostMetadata | null> {
		try {
			const actorId = "apify~instagram-scraper";
			const runUrl = `https://api.apify.com/v2/acts/${actorId}/run-sync-get-dataset-items?token=${this.apifyApiKey}`;

			const resp = await fetch(runUrl, {
				method: "POST",
Confidence
93% confidence
Finding
This request sends a user-supplied Instagram post URL to Apify for scraping. Because the URL may reflect a specific creator, venue, or user-curated content of interest, the code leaks user-provided input to an external service without any visible safeguards in this component, creating privacy and data-governance risk.

External Transmission

Medium
Category
Data Exfiltration
Content
private async getTiktokFromApify(url: string): Promise<PostMetadata | null> {
		try {
			const actorId = "clockworks~tiktok-scraper";
			const runUrl = `https://api.apify.com/v2/acts/${actorId}/run-sync-get-dataset-items?token=${this.apifyApiKey}`;

			const resp = await fetch(runUrl, {
				method: "POST",
Confidence
93% confidence
Finding
This call sends a TikTok post URL to Apify and also requests downloadable video media, increasing the amount and sensitivity of third-party processing. In the context of a skill that collects and analyzes social posts for saved venues, transmitting both the source URL and enabling video download deepens privacy exposure and raises legal/compliance concerns around unnecessary data collection.

External Transmission

Medium
Category
Data Exfiltration
Content
// Include location in the text query for better results
		const textQuery = locationHint ? `${query} in ${locationHint}` : query;

		const resp = await fetch("https://places.googleapis.com/v1/places:searchText", {
			method: "POST",
			headers: {
				"Content-Type": "application/json",
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 breaker = new CircuitBreaker({ threshold: 5, resetTimeMs: 60000 });
 *
 * const result = await breaker.execute(async () => {
 *   return await fetch('https://api.example.com/data');
 * });
 *
 * if (result === null && breaker.isOpen()) {
Confidence
50% 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 breaker = new CircuitBreaker({ threshold: 5, resetTimeMs: 60000 });
 *
 * const result = await breaker.execute(async () => {
 *   return await fetch('https://api.example.com/data');
 * });
 *
 * if (result === null && breaker.isOpen()) {
Confidence
50% confidence
Finding
Data is being sent to an external URL. This could be legitimate telemetry or data exfiltration. Manual review is recommended.

Unsafe Defaults

Medium
Category
Tool Misuse
Content
let city = "";
let category: "restaurant" | "bar" | "cafe" | "event" = "restaurant";
let notes = "";
let verify = false;
let sourceUrl = "";

for (let i = 0; i < args.length; i++) {
Confidence
75% confidence
Finding
Tool defaults are unsafe or overly permissive (e.g. disabled TLS verification, no authentication, world-writable permissions). Unsafe defaults widen the attack surface.

Description-Behavior Mismatch

Low
Confidence
95% confidence
Finding
The manifest description limits the skill's purpose to restaurants, bars, and cafes. However, the documented supported categories explicitly include 'event', which expands the skill's behavior beyond the stated scope.

Known Vulnerable Dependency: @ai-sdk/provider-utils==4.0.19 — 1 advisory(ies): CVE-2026-8769 (@ai-sdk/provider-utils has an Uncontrolled Resource Consumption issue)

Low
Category
Supply Chain
Confidence
85% confidence
Finding
The lockfile pins @ai-sdk/provider-utils to version 4.0.19, which the supplied advisory identifies as affected by uncontrolled resource consumption. In an AI-enabled skill, dependency code may process streamed/model responses or schema validation paths, so a crafted or unusually large input could potentially trigger excessive CPU or memory use and degrade availability.

Unpinned Dependencies

Low
Category
Supply Chain
Content
"format": "biome format --write scripts/"
	},
	"dependencies": {
		"better-sqlite3": "^11.0.0",
		"date-fns": "^4.1.0",
		"date-fns-tz": "^3.2.0",
		"zod": "^3.23.0"
Confidence
40% confidence
Finding
Dependencies lack version pinning, allowing potential malicious package updates. Consider pinning versions.

Unpinned Dependencies

Low
Category
Supply Chain
Content
},
	"dependencies": {
		"better-sqlite3": "^11.0.0",
		"date-fns": "^4.1.0",
		"date-fns-tz": "^3.2.0",
		"zod": "^3.23.0"
	},
Confidence
40% confidence
Finding
Dependencies lack version pinning, allowing potential malicious package updates. Consider pinning versions.

Unpinned Dependencies

Low
Category
Supply Chain
Content
"dependencies": {
		"better-sqlite3": "^11.0.0",
		"date-fns": "^4.1.0",
		"date-fns-tz": "^3.2.0",
		"zod": "^3.23.0"
	},
	"devDependencies": {
Confidence
40% confidence
Finding
Dependencies lack version pinning, allowing potential malicious package updates. Consider pinning versions.

Static analysis

Detected: suspicious.insecure_tls_verification

HTTPS certificate verification is disabled.

Warn
Code
suspicious.insecure_tls_verification
Location
scripts/save-place.ts:18