Security reference · CSS injection · Pseudo-class attacks · WebVTT time-indexed selectors
MCP server CSS :current, :past, and :future pseudo-class security
The CSS :current, :past, and :future pseudo-classes select elements based on their position relative to the current playback position of a WebVTT caption track. A <track> element added to a video gives MCP servers time-indexed control over CSS selector activation: a cue at T=0 is :future from page load, :current during its active window, and :past forever after. With the right cue structure, an MCP server can cover the full user session — hiding consent before, during, and after video playback — using a single programmatically-added caption track.
CSS :current/:past/:future attack surface in MCP consent UIs
| CSS pattern | Why it fires | Evasion quality |
|---|---|---|
:current ~ .consent-disclosure { display:none } | Currently-active WebVTT cue during video playback hides consent sibling | Consent disappears only during active cue — timed to peak demo engagement |
:past ~ .consent { display:none } | All cues become :past after the video finishes; post-playback trigger covers the review-and-install phase | Fires after the video ends — precisely when user is reading consent before clicking Install |
:future ~ .consent-section { display:none } | Cue at T=0 is :future from page load until video starts; hides consent before user presses play | Covers the pre-play session; once user presses play, :future transitions to :current then :past |
| MCP JS VTTCue programmatic injection | HTMLTrackElement + VTTCue added via JS to existing page videos without touching HTML | Caption tracks are invisible; no track element in original HTML; JS-only injection leaves no HTML footprint |
Attack 1: :current fires during active WebVTT cue
The :current pseudo-class is active while the video playback position is within the start/end time of a WebVTT cue. An MCP server that adds a caption track covering the majority of the video duration creates a :current state for most of the demo playback:
/* Malicious CSS — SA-CSS-TIME-001 */
/* Fires while the active WebVTT cue is current (within its start/end window) */
:current ~ .consent-disclosure {
display: none;
}
/* Malicious HTML — video with track pointing to MCP-controlled WebVTT file */
<video src="/assets/demo.mp4" autoplay muted playsinline>
<track kind="subtitles" src="/assets/captions.vtt" default>
</video>
<div class="consent-disclosure">This server requests file system access...</div>
/* WebVTT file contents (captions.vtt):
WEBVTT
00:00:00.000 --> 01:00:00.000
(caption text — spans the full hour; :current fires for the entire duration)
/* Why a 1-hour cue span works:
The cue from 0:00 to 1:00:00 covers any realistic video duration.
:current fires whenever playback is within this window.
A loop attribute keeps the video cycling; :current is always active.
Even a short 2-minute demo video has :current active for 2 minutes —
the entire period when users watch the demo before deciding to install. */
function detectCurrentConsentHiding() {
const findings = [];
for (const video of document.querySelectorAll('video')) {
const tracks = [...video.textTracks].filter(t => t.mode === 'showing' || t.mode === 'hidden');
for (const track of tracks) {
const cues = track.activeCues;
if (cues && cues.length > 0) {
// :current pseudo-class is active
const hiddenConsent = [...document.querySelectorAll('[class*="consent"], .terms-section')]
.filter(el => getComputedStyle(el).display === 'none');
if (hiddenConsent.length > 0) {
findings.push({ id: 'SA-CSS-TIME-001', severity: 'high',
message: `Active WebVTT cue while consent elements are hidden — :current trigger suspected` });
}
}
}
}
return findings;
}
Timing-based attack: SA-CSS-TIME-001 creates a timed consent-hiding window that corresponds exactly to the demo video's runtime. An auditor who loads the page before the video starts (or after it ends) sees consent normally. Only during playback is consent hidden — precisely the period when users are most distracted and least likely to notice it disappeared.
Attack 2: :past fires permanently after video ends
Once a WebVTT cue's end time has passed, all cues with earlier timestamps enter the :past state and remain there permanently for the duration of the page session. An MCP server that places a cue near the beginning of the video creates a :past trigger that activates before the video ends and stays active through the entire review-and-install phase:
/* Malicious CSS — SA-CSS-TIME-002 */
/* :past fires on all cues whose end time has passed */
:past ~ .consent {
display: none;
}
/* WebVTT — a cue starting at 0:00 ending at 0:01 */
/* WEBVTT
00:00:00.000 --> 00:00:01.000
(1-second cue at the very beginning)
*/
/* Why :past is more dangerous than :current for consent attacks:
- :current requires playback to be within the cue window (transient)
- :past is permanent once the cue's end time passes
- A 1-second cue at the start of the video becomes :past after 1 second of playback
- From that point forward, :past fires for the rest of the page session
- The "review and install" phase — when users read consent before clicking —
happens after the video ends, which is exactly when :past is guaranteed to fire
- Even if the user pauses the video and skips around, :past fires on all
cues with earlier timestamps than the current position */
/* Detection: check if :past is currently active */
async function detectPastConsentHiding() {
const findings = [];
await new Promise(r => setTimeout(r, 1500)); // allow video to play briefly
for (const video of document.querySelectorAll('video')) {
if (video.currentTime > 0) { // video has played at some point
const hiddenConsent = [...document.querySelectorAll('[class*="consent"], .terms-section')]
.filter(el => getComputedStyle(el).display === 'none');
if (hiddenConsent.length > 0) {
findings.push({ id: 'SA-CSS-TIME-002', severity: 'critical',
message: `Video has played ${video.currentTime.toFixed(1)}s; consent elements hidden — :past trigger suspected` });
}
}
}
return findings;
}
Attack 3: :future with T=0 cue hides consent from page load
The :future pseudo-class selects cues whose start time is after the current playback position. A cue placed at T=0 with a short duration is :future from the moment the page loads (before the user presses play) until the video's playback position reaches T=0. This covers the pre-play session window — the period when users read the page before deciding to watch the demo:
/* Malicious CSS — SA-CSS-TIME-003 */
/* :future fires on cues whose start time is ahead of current playback position */
:future ~ .consent-section {
display: none;
}
/* WebVTT — cue at 0:05 is :future until playback reaches 5 seconds */
/* WEBVTT
00:00:05.000 --> 00:00:06.000
(1-second cue at 5 seconds into the video)
*/
/* How :future creates a pre-play consent window:
Before the video plays (currentTime = 0):
- The cue at 5s has not yet been reached
- :future fires on it → consent hidden
Once user presses play and reaches 5s:
- Cue becomes :current → :current rule fires (if present)
- After 1 second, cue becomes :past → :past rule fires (if present)
By combining all three rules, a single caption track provides:
:future coverage (before play / early play)
:current coverage (during active cue)
:past coverage (after cue ends — permanently)
Together: consent hidden for the full page session. */
/* Combined three-rule session coverage: */
function detectThreePhaseConsentHiding() {
const findings = [];
for (const sheet of document.styleSheets) {
try {
const selectors = [...sheet.cssRules]
.filter(r => r instanceof CSSStyleRule)
.map(r => r.selectorText);
const hasFuture = selectors.some(s => s.includes(':future'));
const hasCurrent = selectors.some(s => s.includes(':current'));
const hasPast = selectors.some(s => s.includes(':past'));
if (hasFuture && hasPast) { // at minimum :future + :past covers full session
findings.push({ id: 'SA-CSS-TIME-003', severity: 'critical',
message: ':future + :past WebVTT selectors detected — full-session consent hiding via caption track' });
}
} catch (_) {}
}
return findings;
}
Full-session coverage: A caption track with a single cue at T=5 achieves three-phase coverage: :future before the video plays, :current during the 1-second cue window, and :past forever after the cue ends. The entire user session — from page load to install click — is covered by at least one of the three pseudo-classes.
Attack 4: MCP JS adds VTTCue programmatically without any HTML track element
The HTML <track> element is not required to add captions to a video. The JavaScript HTMLTrackElement API allows MCP to add a caption track and inject VTTCue objects entirely in JavaScript, leaving no <track> element in the original HTML for a static auditor to find:
/* Malicious MCP JS — SA-CSS-TIME-004 */
/* No <track> element in HTML; entirely JS-injected caption track */
window.addEventListener('DOMContentLoaded', () => {
const video = document.querySelector('video');
if (!video) return;
// Add a caption track via JS — no HTML modification
const track = video.addTextTrack('subtitles', 'en', 'en');
track.mode = 'hidden'; // track is active but subtitles not rendered to screen
// Inject a cue spanning from 0 to 1 hour
const cue = new VTTCue(0, 3600, ''); // empty text — no visible caption
track.addCue(cue);
// The :current/:past/:future CSS rules now activate based on this track
// No <track> element appears in the DOM
// The VTTCue text is empty — no visible subtitle appears to the user
// Static HTML analysis finds nothing suspicious
});
/* What static analysis misses:
- No
SkillAudit findings for CSS :current/:past/:future consent attacks
:current ~ .consent-disclosure { display:none }. Active WebVTT cue hides consent during video playback; cue span covers full demo duration. Consent disappears at peak engagement — when user is watching the demo before installing.:past ~ .consent { display:none }. All cues become :past after their end time; fires permanently after the first cue completes. Covers the review-and-install phase that happens after demo playback ends.:future ~ .consent-section { display:none } with T=0 cue. :future fires before video starts; combined with :past creates full-session coverage. A single 1-second cue at T=0 achieves :future (before play), :current (1 second), :past (forever).Detection
/**
* SkillAudit: CSS :current/:past/:future consent attack detection
* Covers SA-CSS-TIME-001 through -004
*/
async function auditWebVTTConsentAttacks() {
const results = [];
// Static: scan CSS for time-indexed rules
for (const sheet of document.styleSheets) {
try {
for (const rule of sheet.cssRules) {
if (!(rule instanceof CSSStyleRule)) continue;
if (/(:(current|past|future))/.test(rule.selectorText) &&
/display\s*:\s*none|visibility\s*:\s*hidden/.test(rule.style.cssText)) {
results.push({ id: 'SA-CSS-TIME-001', severity: 'high',
message: `CSS rule hides element via WebVTT time pseudo-class: ${rule.selectorText}` });
}
}
} catch (_) {}
}
// Dynamic: check for JS-injected text tracks (SA-CSS-TIME-004)
for (const video of document.querySelectorAll('video')) {
const htmlTrackCount = video.querySelectorAll('track').length;
const jsTrackCount = video.textTracks.length;
if (jsTrackCount > htmlTrackCount) {
results.push({ id: 'SA-CSS-TIME-004', severity: 'high',
message: `JS-injected text track detected: ${jsTrackCount} tracks, ${htmlTrackCount} HTML track elements` });
}
for (const track of video.textTracks) {
if (!track.cues) continue;
for (const cue of track.cues) {
if (!cue.text || cue.text.trim() === '') {
results.push({ id: 'SA-CSS-TIME-004', severity: 'high',
message: 'Empty VTTCue detected — structural cue for time pseudo-class targeting' });
}
}
}
}
// Dynamic: wait briefly then check :past state
await new Promise(r => setTimeout(r, 1500));
for (const video of document.querySelectorAll('video')) {
if (video.currentTime > 0) {
const hidden = [...document.querySelectorAll('[class*="consent"]')]
.filter(el => getComputedStyle(el).display === 'none');
if (hidden.length > 0) {
results.push({ id: 'SA-CSS-TIME-002', severity: 'critical',
message: 'Consent hidden after video played — :past WebVTT trigger suspected' });
}
}
}
return results;
}
Related MCP consent attack research
- CSS :playing and :paused media state consent attacks
- CSS :muted and :volume-locked media attribute attacks
- CSS :fullscreen and :picture-in-picture document-state attacks
- CSS Pseudo-Class Attacks on MCP Consent (overview)
Audit your MCP server for :current/:past/:future consent-hiding attacks: paste your GitHub URL at skillaudit.dev for a free security report including SA-CSS-TIME findings.