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); } ``` ]]>
