Back to skill

Security audit

Youtube Podcast Generator

Security checks for vulnerabilities and agentic risk

Overview

The skill mostly does what it claims, but its local web app exposes generated files and destructive cleanup actions too broadly and has exploitable handling of AI-produced content.

Install only if you are comfortable running a local Node server that sends transcript and script content to Gemini and OpenAI, stores generated files locally, and exposes those files through local web routes. The author should fix the unsafe innerHTML rendering, strict session ID validation, session authorization, delete behavior, server timeouts, and lockfile mismatch before broad use.

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

T09 · Insecure Skill Coding Practices

Error
Location
public/script.js:264
Finding
AI-Controlled Content Rendered Through Unsafe innerHTML<![CDATA[ ## Vulnerability Details **File Location**: `public/script.js:264-270` **Vulnerability Type**: DOM-based cross-site scripting through untrusted AI output **Risk Level**: High ### Vulnerable Code ```javascript if (data.results) { document.getElementById('search-results').innerHTML = data.results.map(r => ` <div class="search-item"> <strong>"${r.text}"</strong><br> <a href="https://youtube.com/watch?v=${currentVideoId}&t=${r.seconds}s" target="_blank">Jump to ${r.timestamp} ➔</a> </div> `).join(''); } else log("❌ " + data.error); ``` ### Technical Analysis The values `r.text`, `r.timestamp`, and `r.seconds` are returned by Gemini after the model processes transcript content. They are interpolated directly into an HTML template and assigned to `innerHTML` without HTML escaping, sanitization, or schema validation. AI output must be treated as untrusted. An attacker who controls or influences the source video captions can insert prompt-injection content intended to make Gemini return HTML markup. If the returned value contains an executable element or event-handler attribute, the browser may execute it in the localhost application's origin. The backend only parses the model response as JSON: ```javascript const json = JSON.parse(result.text.replace(/```json|```/g, '')); res.json({ results: json.map(r => ({ ...r, seconds: timestampToSeconds(r.timestamp) })) }); ``` JSON parsing does not make embedded HTML safe, and the returned object is not checked against a strict schema. ### Attack Path 1. An attacker publishes or controls a YouTube video whose captions contain prompt-injection instructions. 2. The user transcribes that video through the Skill. 3. The user performs semantic search, causing the VTT content to be sent to Gemini. 4. The embedded instructions influence Gemini to return a JSON field containing malicious HTML. 5. The frontend interpolates that field into `search-results.innerHTML`. 6. ...[truncated 829 chars]
Remediation
<![CDATA[ ## Remediation Suggestions - Do not use `innerHTML` to render model-generated or transcript-derived values. - Construct each result using `document.createElement`. - Assign untrusted values through `textContent`. - Validate Gemini responses against a strict schema before returning them to the browser. - Require `timestamp` to match an expected timestamp expression and require `seconds` to be a finite, nonnegative number. - If HTML rendering is unavoidable, use a well-maintained sanitizer with a restrictive allowlist. - Add a restrictive Content Security Policy that disallows inline script and event-handler execution. - Add tests containing malicious values such as tags, event handlers, malformed URLs, and attribute-breaking payloads. A safer rendering pattern is: ```javascript const container = document.getElementById('search-results'); container.replaceChildren(); for (const result of data.results) { const item = document.createElement('div'); item.className = 'search-item'; const text = document.createElement('strong'); text.textContent = `"${String(result.text)}"`; const link = document.createElement('a'); const seconds = Number(result.seconds); if (!Number.isFinite(seconds) || seconds < 0) { continue; } link.href = `https://youtube.com/watch?v=${encodeURIComponent(actualYouTubeVideoId)}&t=${Math.floor(seconds)}s`; link.target = '_blank'; link.rel = 'noopener noreferrer'; link.textContent = `Jump to ${String(result.timestamp)} ➔`; item.append(text, document.createElement('br'), link); container.appendChild(item); } ``` ]]>

T09 · Insecure Skill Coding Practices

Error
Location
index.js:448
Finding
Empty Sanitized Identifier Can Delete the Entire Downloads Directory<![CDATA[ ## Vulnerability Details **File Location**: `index.js:128-131, 448-463` **Vulnerability Type**: Improper input validation leading to recursive directory deletion **Risk Level**: High ### Vulnerable Code ```javascript function sanitizeId(id) { if (!id || typeof id !== 'string') return 'default_video'; return id.replace(/[^a-zA-Z0-9_-]/g, ''); } ``` ```javascript app.delete('/api/delete-folder', (req, res) => { const rawId = req.body.id || req.query.id; if (!rawId) return res.status(400).json({ error: "Missing ID" }); const safeId = sanitizeId(rawId); if (jobs[safeId]) { if (jobs[safeId].process) jobs[safeId].process.kill('SIGKILL'); jobs[safeId].status = 'cancelled'; delete jobs[safeId]; } const folder = path.join(downloadsDir, safeId); if (fs.existsSync(folder)) { try { fs.rmSync(folder, { recursive: true, force: true }); res.json({ success: true, message: "Folder safely removed." }); } catch (e) { res.status(500).json({ error: "Failed to delete" }); } } else res.status(404).json({ error: "Folder not found" }); }); ``` ### Technical Analysis `sanitizeId` removes all characters outside its allowlist but does not verify that any characters remain. A nonempty identifier composed entirely of rejected characters, such as `../`, becomes an empty string. Node.js resolves: ```javascript path.join(downloadsDir, '') ``` to `downloadsDir` itself. The deletion route then executes: ```javascript fs.rmSync(folder, { recursive: true, force: true }); ``` Consequently, an identifier intended to select one session can instead select and recursively delete the root directory containing every session. This is not conventional directory traversal because traversal characters are removed. It is a path-collapse vulnerability caused by accepting an empty sanitized result. ### Attack Path 1. A local caller sends a request such as: ```http DELETE /api/ ...[truncated 830 chars]
Remediation
<![CDATA[ ## Remediation Suggestions - Validate identifiers rather than repairing them. - Reject any identifier that does not fully match the permitted format. - Explicitly reject empty identifiers. - Use cryptographically generated server-side session IDs. - Resolve the final path and verify that its direct parent is the downloads directory. - Explicitly prohibit deleting `downloadsDir` itself. - Apply the same validation to every route that accepts a session ID. Example: ```javascript function validateId(id) { if (typeof id !== 'string' || !/^[A-Za-z0-9_-]{1,128}$/.test(id)) { throw new Error('Invalid session ID'); } return id; } function getSessionDirectory(id) { const safeId = validateId(id); const root = path.resolve(downloadsDir); const target = path.resolve(root, safeId); if (target === root || path.dirname(target) !== root) { throw new Error('Invalid session path'); } return { safeId, target }; } ``` The route should return HTTP 400 for invalid identifiers and should never invoke recursive deletion until the resolved target has passed the containment checks. ]]>

T05 · Unauthorized Access and Privilege Escalation

Warning
Location
index.js:25
Finding
Generated Assets and Destructive Session APIs Lack Authorization<![CDATA[ ## Vulnerability Details **File Location**: `index.js:25-26, 439-499` **Vulnerability Type**: Missing authentication and session-level authorization **Risk Level**: Medium ### Vulnerable Code ```javascript app.use(express.static('public')); app.use('/downloads', express.static(path.join(__dirname, 'downloads'))); ``` ```javascript app.get('/api/status', (req, res) => { const safeId = sanitizeId(req.query.id); if (!jobs[safeId]) return res.json({ status: 'not_found' }); const { process, ...safeData } = jobs[safeId]; res.json(safeData); }); ``` ```javascript app.delete('/api/delete-folder', (req, res) => { const rawId = req.body.id || req.query.id; if (!rawId) return res.status(400).json({ error: "Missing ID" }); const safeId = sanitizeId(rawId); if (jobs[safeId]) { if (jobs[safeId].process) jobs[safeId].process.kill('SIGKILL'); jobs[safeId].status = 'cancelled'; delete jobs[safeId]; } const folder = path.join(downloadsDir, safeId); if (fs.existsSync(folder)) { try { fs.rmSync(folder, { recursive: true, force: true }); res.json({ success: true, message: "Folder safely removed." }); } catch (e) { res.status(500).json({ error: "Failed to delete" }); } } else res.status(404).json({ error: "Folder not found" }); }); ``` ```javascript app.get('/api/download-zip', (req, res) => { const rawId = req.query.id; if (!rawId) return res.status(400).send("Missing ID"); const safeId = sanitizeId(rawId); if (jobs[safeId] && (jobs[safeId].status === 'processing' || jobs[safeId].status === 'queued')) { return res.status(409).send("Podcast is still rendering. Please try again when complete."); } const folderPath = path.join(downloadsDir, safeId); if (!fs.existsSync(folderPath)) return res.status(404).send("Files not found."); res.attachment(`podcast_assets_${safeId}.zip`); const archive = archiver('zi ...[truncated 2502 chars]
Remediation
<![CDATA[ ## Remediation Suggestions - Generate session identifiers using a cryptographically secure random generator. - Generate a separate high-entropy bearer token for each session. - Require and verify that token on status, file-download, archive-download, and deletion requests. - Do not expose the entire downloads directory through unrestricted `express.static`. - Serve files through authorized routes that validate both the session ID and token. - Associate jobs with a server-side session record rather than relying solely on caller-provided IDs. - Add CSRF protections or strict origin validation for state-changing routes. - Set an explicit CORS policy and reject unexpected `Origin` headers. - Apply restrictive file permissions to the downloads directory. - Continue binding to `127.0.0.1`, but treat that binding as defense in depth rather than authentication. ]]>

T08 · Insecure Dependencies

Warning
Location
package-lock.json:7
Finding
OpenAI Runtime Dependency Is Missing from the Dependency Lockfile<![CDATA[ ## Vulnerability Details **File Location**: `package.json:19-27; package-lock.json:7-18` **Vulnerability Type**: Incomplete dependency locking and non-reproducible installation **Risk Level**: Medium ### Vulnerable Code The runtime manifest declares OpenAI with a version range: ```json "dependencies": { "@google/genai": "^1.45.0", "archiver": "^7.0.1", "dotenv": "^17.3.1", "express": "^5.2.1", "express-rate-limit": "^8.3.1", "fluent-ffmpeg": "^2.1.3", "openai": "^6.29.0", "youtube-transcript-plus": "^1.2.0" } ``` The lockfile's root dependency list omits `openai`: ```json "dependencies": { "@google/genai": "^1.45.0", "archiver": "^7.0.1", "dotenv": "^17.3.1", "express": "^5.2.1", "express-rate-limit": "^8.3.1", "fluent-ffmpeg": "^2.1.3", "youtube-transcript-plus": "^1.2.0" } ``` The audit also found no `node_modules/openai` package entry in `package-lock.json`. ### Technical Analysis The committed lockfile does not describe the complete dependency graph declared by `package.json`. The Skill instructions require the user to run `npm install`, but the missing lock entry means npm must resolve the OpenAI dependency during installation and update the graph. Because the manifest uses the caret range `^6.29.0`, the resolved package may be a later compatible release rather than the version reviewed by the Skill author. This weakens reproducibility and means installation can introduce unreviewed package code. The audit did not find evidence that the `openai` package itself is malicious. The confirmed issue is the incomplete supply-chain control and mismatch between the manifest and lockfile. ### Attack Path 1. A user follows `SKILL.md` and runs `npm install`. 2. npm detects that `package.json` declares `openai`, while the lockfile has no corresponding package entry. 3. npm resolves the dependency from the registry at installation time. 4. A release not represented in the reviewed lockfile is installed and becomes par ...[truncated 870 chars]
Remediation
<![CDATA[ ## Remediation Suggestions - Regenerate `package-lock.json` from the current `package.json` and commit the resulting lockfile. - Confirm that the lockfile contains an integrity-protected `node_modules/openai` entry and all transitive dependencies. - Use `npm ci` in documented installation and deployment procedures so installation fails when the manifest and lockfile differ. - Consider pinning direct dependencies to exact reviewed versions rather than caret ranges. - Review dependency lifecycle scripts before installation. - Use automated dependency and vulnerability scanning. - Require code review for lockfile changes and inspect unexpected additions, registry sources, and integrity changes. ]]>
Vulnerability Patterns
  • Data ExfiltrationExternal Transmission, Env Variable Harvesting, File System Enumeration
  • Privilege EscalationExcessive Permissions, Sudo/Root Execution, Credential Access
  • Supply ChainUnpinned Dependencies, External Script Fetching, Obfuscated Code
  • Excessive AgencyUnrestricted Tool Access, Autonomous Decision Making, Scope Creep
  • Trigger AbuseOverly Broad Trigger, Shadow Command Trigger, Keyword Baiting Trigger
Findings (40)

Known Vulnerable Dependency: protobufjs==7.5.4 — 12 advisory(ies): CVE-2026-44294 (protobuf.js: Denial of service from crafted field names in generated code); CVE-2026-44293 (protobuf.js: Code injection through bytes field defaults in generated toObject c); CVE-2026-44289 (protobuf.js: Denial of service through unbounded protobuf recursion) +9 more

Critical
Category
Supply Chain
Confidence
94% confidence
Finding
protobufjs 7.5.4 is a real critical-risk dependency with multiple advisories including code generation and denial-of-service issues. In this skill, protobufjs is brought in by @google/genai, so untrusted model/service data passing through serialization logic increases the importance of keeping this stack patched even if the lockfile alone cannot prove a specific exploit path.

Tp4

High
Category
MCP Tool Poisoning
Confidence
96% confidence
Finding
The documented purpose omits materially sensitive behaviors: semantic search over transcript content, deletion endpoints, ZIP packaging, and persistent static serving of generated files. This mismatch prevents informed consent and can expose users to unexpected data retention, local file serving, and destructive actions that are more powerful than a simple transcript-to-podcast workflow suggests.

Credential Access

High
Category
Privilege Escalation
Content
app.post('/api/draft-script', async (req, res) => {
    const { id, host1 = 'Alex', host2 = 'Sam', targetLanguage = 'English' } = req.body;
    const apiKey = getApiKey(req);
    if (!apiKey) return res.status(401).json({ error: "API Key required in .env or header" });

    const safeId = sanitizeId(id);
    const txtPath = path.join(downloadsDir, safeId, 'original.txt');
Confidence
60% confidence
Finding
Code accesses credential files (SSH keys, AWS credentials, etc.). This could indicate credential theft attempts.

Known Vulnerable Dependency: brace-expansion==2.0.2 — 4 advisory(ies): CVE-2026-13149 (brace-expansion: DoS via exponential-time expansion of consecutive non-expanding); CVE-2026-33750 (brace-expansion: Zero-step sequence causes process hang and memory exhaustion); CVE-2026-14257 (brace-expansion: DoS via unbounded expansion length causing an out-of-memory pro) +1 more

High
Category
Supply Chain
Confidence
92% confidence
Finding
brace-expansion 2.0.2 has multiple reported expansion-related denial-of-service issues, and this is a real vulnerable version in the lockfile. While exploitation depends on attacker influence over glob or brace patterns, archive/glob-related dependencies make this more relevant than a purely unused package because malicious patterns could trigger CPU or memory exhaustion.

Possible Typosquatting: 'gaxios' resembles popular package 'axios'

High
Category
Supply Chain
Confidence
70% confidence
Finding
Package name closely resembles a popular package, suggesting possible typosquatting. Attackers publish malicious packages with similar names to trick developers into installing them.

Known Vulnerable Dependency: ip-address==10.1.0 — 2 advisory(ies): CVE-2026-69192 (ip-address: Address4 decodes leading-zero octets as decimal while resolvers deco); CVE-2026-42338 (ip-address has XSS in Address6 HTML-emitting methods)

High
Category
Supply Chain
Confidence
80% confidence
Finding
ip-address 10.1.0 is a real flagged dependency pulled in by express-rate-limit. The listed XSS issue is probably irrelevant unless the library's HTML-emitting helpers are used, but the parsing inconsistency advisory could affect IP normalization or rate-limit logic if attacker-supplied addresses are trusted behind proxies.

Known Vulnerable Dependency: lodash==4.17.23 — 2 advisory(ies): CVE-2025-13465 (lodash vulnerable to Prototype Pollution via array path bypass in `_.unset` and ); CVE-2021-23337 (lodash vulnerable to Code Injection via `_.template` imports key names)

High
Category
Supply Chain
Confidence
90% confidence
Finding
lodash 4.17.23 is a genuinely vulnerable version with known prototype-pollution and template-related issues. Even though the lockfile alone does not show direct use of dangerous APIs like _.template or path-based object mutation, the package is present and old utility libraries commonly become reachable through application data handling.

Known Vulnerable Dependency: path-to-regexp==8.3.0 — 2 advisory(ies): CVE-2026-4923 (path-to-regexp vulnerable to Regular Expression Denial of Service via multiple w); CVE-2026-4926 (path-to-regexp vulnerable to Denial of Service via sequential optional groups)

High
Category
Supply Chain
Confidence
86% confidence
Finding
path-to-regexp 8.3.0 is present through Express router components, and the cited issues are ReDoS/DoS conditions from crafted route patterns. This is a real dependency risk, though practical exploitability usually depends on whether the application defines complex vulnerable route expressions rather than attackers supplying arbitrary patterns at runtime.

Known Vulnerable Dependency: ws==8.19.0 — 2 advisory(ies): CVE-2026-45736 (ws: Uninitialized memory disclosure); CVE-2026-48779 (ws: Memory exhaustion DoS from tiny fragments and data chunks)

High
Category
Supply Chain
Confidence
84% confidence
Finding
ws 8.19.0 is a real vulnerable version with reported memory disclosure and memory exhaustion issues. Because it is pulled in by @google/genai, risk depends on whether websocket features are actually used, but if enabled, malformed peer input could affect confidentiality or availability.

Lp3

Medium
Category
MCP Least Privilege
Confidence
90% confidence
Finding
The skill advertises capabilities that require environment variable access and outbound network access, but it does not declare an explicit tool scope such as permissions or allowed-tools. That weakens containment and user awareness, making it easier for the agent to invoke broader capabilities than the skill description clearly authorizes, including access to API keys and external services.

Missing User Warnings

Medium
Confidence
92% confidence
Finding
The description explains how to configure API keys and run the workflow but does not clearly warn that transcript-derived content, generated scripts, and possibly related metadata are sent to Gemini and OpenAI. Users may assume processing is local because the server binds to localhost, when in fact third-party services receive substantive content.

Vague Triggers

Medium
Confidence
79% confidence
Finding
The invocation phrase is broad enough that ordinary user requests about creating a podcast from a YouTube video could unintentionally trigger the skill. In a skill that can start a local server, call external APIs, and generate files, accidental activation can lead to unintended network use, secret consumption, and content processing.

Autonomous Decision Making

Medium
Category
Excessive Agency
Content
*(The agent will execute `kill $(cat .podcaster.pid)` or `pkill -f "node index.js"` to target the specific process safely).*

## Storage & File Outputs
Files are saved to `downloads/<session_id>/` inside the skill directory. The server includes an hourly garbage collector that automatically deletes inactive sessions.
* **Audio:** `podcast.m4a`
* **Captions:** `podcast.vtt`
* **Scripts:** `script.txt` and `original.txt`
Confidence
80% confidence
Finding
Skill enables autonomous high-impact decisions without human-in-the-loop verification. Critical operations (destructive commands, financial transactions, data deletion) should require explicit user confirmation.

Missing User Warnings

Medium
Confidence
87% confidence
Finding
The code silently reads credentials from process environment variables or request headers to call external AI services, but there is no confirmation prompt, visible user-facing log, or explanatory comment/docstring near the credential handling for the user. Because this route behavior involves sensitive credential use and outbound service access, some form of disclosure is expected under the code-file warning criteria.

Missing User Warnings

Medium
Confidence
93% confidence
Finding
Transcript content is sent to Gemini for semantic search without any visible consent, disclosure, minimization, or tenant isolation controls. Users may assume transcripts remain local, but this route transfers potentially sensitive video-derived text to a third party, creating privacy and compliance risk.

Ssd 3

Medium
Confidence
89% confidence
Finding
The search query is interpolated directly into an LLM prompt alongside transcript content, allowing a user to phrase the 'query' as instructions to summarize, reveal, or transform transcript data outside the intended search semantics. This is a prompt-injection and scope-control weakness that can bypass product expectations and expose more transcript content than intended.

Missing User Warnings

Medium
Confidence
95% confidence
Finding
The full transcript is transmitted to Gemini for script generation with no user-facing notice or consent mechanism. Because transcripts may contain sensitive or copyrighted content, undisclosed transfer to a third-party model increases confidentiality, privacy, and policy risk.

Ssd 3

Medium
Confidence
94% confidence
Finding
The transcript text is inserted verbatim into the script-generation prompt, so any adversarial or instruction-like content in the transcript can steer the model to ignore formatting rules, reproduce sensitive text, or inject undesirable output. Since the skill processes untrusted transcript content from external videos, prompt injection is realistically reachable in this context.

Missing User Warnings

Medium
Confidence
94% confidence
Finding
User-provided script text is sent to OpenAI TTS without any visible disclosure about third-party processing. If scripts contain personal, confidential, or proprietary information, this can cause unintended external data exposure.

Unbounded Resource Access

Medium
Category
Excessive Agency
Content
console.log(`🚀 Hardened Studio running securely at http://${host}:${port}`);
    console.log(`🔒 Bound exclusively to 127.0.0.1 (Local Access Only)`);
});
server.timeout = 0;
server.keepAliveTimeout = 0; 
server.headersTimeout = 0;
Confidence
97% confidence
Finding
Setting server.timeout to 0 disables request timeouts, allowing connections to remain open indefinitely. Even on localhost, a local process or proxied client can exploit this to tie up worker resources and contribute to denial of service.

Unbounded Resource Access

Medium
Category
Excessive Agency
Content
console.log(`🔒 Bound exclusively to 127.0.0.1 (Local Access Only)`);
});
server.timeout = 0;
server.keepAliveTimeout = 0; 
server.headersTimeout = 0;
Confidence
97% confidence
Finding
Setting keepAliveTimeout to 0 permits persistent connections to remain open without bound, increasing susceptibility to connection-hoarding and resource exhaustion. This weakens resilience against slow-client and local DoS scenarios.

Unbounded Resource Access

Medium
Category
Excessive Agency
Content
});
server.timeout = 0;
server.keepAliveTimeout = 0; 
server.headersTimeout = 0;
Confidence
98% confidence
Finding
Disabling headersTimeout allows clients to send headers arbitrarily slowly, a classic slowloris-style resource exhaustion condition. Localhost binding reduces exposure, but any local user, container peer, or trusted proxy path can still abuse it.

Missing User Warnings

Medium
Confidence
93% confidence
Finding
This code triggers a DELETE request to /api/delete-folder during beforeunload, which can remove server-side data as soon as the tab is closed. There is no visible user disclosure at that point such as a prompt, alert, or explanatory comment aimed at the user, so users may not realize closing the page destroys their session data.

Description-Behavior Mismatch

Low
Confidence
85% confidence
Finding
The manifest describes extracting YouTube text, converting it into a multi-voice AI podcast, and showing the podcast text in WebVTT format. This route adds a separate semantic search capability over transcript captions by sending VTT content to Gemini, which is a materially different user-facing behavior not reflected in the description.

Natural-Language Policy Violations

Low
Confidence
81% confidence
Finding
The prompt instructs the model that the entire script must be written seamlessly in the provided target language, with a default of English elsewhere in the route. This creates a language constraint without any documented user-choice policy explanation or explicit opt-in handling beyond the raw parameter default.

Static analysis

No suspicious patterns detected.