T09 · Insecure Skill Coding Practices
- Location
assets/storyboard_template.html:394- Finding
DOM-Based HTML Injection in Storyboard Timeline Rendering
- Content
View full analysis
{ const pct = ((shot.duration || 5) / total * 100).toFixed(1); const color = colors[i % colors.length]; return `${i+1}`; }).join(''); ``` ### Technical Analysis The timeline renderer obtains storyboard entries from `window.__SHOT_DATA__` and constructs HTML by interpolating their properties into a template string. The resulting string is assigned to `bar.innerHTML`. In particular, `shot.name` and the displayed `shot.duration` are inserted into the `title` attribute without contextual escaping or sanitization. An attacker who can influence storyboard data can terminate the attribute and inject new HTML attributes or elements. Browser parsing of the resulting `innerHTML` can therefore introduce event handlers or other script-capable markup. The percentage calculation converts the duration to a number before placing it in the `style` width, but the original duration is also inserted directly into the `title` attribute. The conversion used for width calculation does not sanitize the separately interpolated value. ### Attack Path 1. An attacker supplies or modifies storyboard data consumed as `window.__SHOT_DATA__`. 2. The attacker places an attribute-breaking payload in `shot.name`, for example a value containing a quotation mark followed by an event-handler attribute. 3. `renderTimeline()` interpolates that value into the `title` attribute. 4. The generated string is assigned to `bar.innerHTML`. 5. The browser parses the injected markup as active DOM content. 6. The injected event handler executes when its triggering condition occurs, such as us ...[truncated 883 chars]- Remediation
View remediation
{ const value = Number(shot.duration); return Number.isFinite(value) && value > 0 ? value : 5; }); const total = durations.reduce((sum, value) => sum + value, 0); bar.replaceChildren(); shots.forEach((shot, i) => { const duration = durations[i]; const pct = (duration / total * 100).toFixed(1); const segment = document.createElement('div'); segment.className = 'timeline-segment'; segment.style.width = `${pct}%`; segment.style.background = colors[i % colors.length]; const safeName = typeof shot.name === 'string' ? shot.name.slice(0, 200) : `Shot ${i + 1}`; segment.title = `${safeName}: ${duration}s`; segment.textContent = String(i + 1); bar.appendChild(segment); }); } ``` ]]>
