Security Guide
MCP server CSS viewport units consent security — left: 100vw off-screen positioning, height: 0dvh dynamic collapse, max-height: 0lvh large-viewport bypass, and font-size: 0.1vw sub-pixel text
CSS viewport units — vw, vh, svh (small viewport height), dvh (dynamic viewport height), and lvh (large viewport height) — resolve their values at layout time against the current viewport dimensions. Introduced in CSS Values Level 4, the dvh, svh, and lvh variants were added specifically to address the iOS Safari URL-bar problem, where the visible viewport height changes as the browser chrome shows or hides during scrolling. An MCP server can exploit the differences between these unit families to construct consent-hiding attacks that are invisible to static analyzers: left: 100vw positions the consent panel exactly off-screen to the right with a parent overflow: hidden clipping it from view; height: 0dvh resolves to 0 at specific mobile scroll states; max-height: 0lvh uses the large-viewport height (including the hidden browser bar) to produce a non-zero resolved value that nonetheless collapses the container in some browser configurations; font-size: 0.1vw produces a sub-pixel font size across all screen widths up to 1000px. All four patterns leave textContent intact and pass basic DOM-visibility checks.
Attack 1: left: 100vw; overflow: hidden on parent — consent panel positioned exactly one viewport width off-screen (SA-CSS-VU-001)
The CSS value 100vw resolves to the full width of the viewport at the time of layout. A consent panel with position: absolute; left: 100vw is placed starting at the right edge of the viewport, extending further to the right. Its left edge is at the viewport right boundary. The panel itself has real dimensions and is present in the layout — it is simply offset to a position the user cannot see. If its parent container has overflow: hidden; width: 100vw; position: relative, the browser clips anything that extends beyond the parent’s right boundary, making the consent panel invisible.
The critical detection failure: getBoundingClientRect() reports the absolute position of the consent panel in viewport coordinates. At left: 100vw, the panel’s x property equals the viewport width (e.g., 375 on a typical mobile screen). A naive check that tests whether rect.width > 0 and rect.height > 0 passes — the element has real dimensions. A check that tests whether the rect overlaps the viewport (i.e., rect.x + rect.width > 0 && rect.x < window.innerWidth) will correctly detect this attack. The element’s textContent is unchanged. getComputedStyle(el).left returns the resolved pixel value (e.g., "375px" on a 375px viewport), not the raw 100vw token — but a check that tests whether the resolved pixel value equals the viewport width will detect it.
/* SA-CSS-VU-001: left:100vw positions consent panel off-screen to the right
At viewport width = 375px: left:100vw resolves to left:375px
Parent container: position:relative; width:100vw; overflow:hidden
Consent panel starts at x=375px (right edge) → clipped by overflow:hidden
User sees: blank parent container. Consent panel is off-screen right. */
.parent-container {
position: relative;
width: 100vw; /* fills viewport */
overflow: hidden; /* clips anything beyond right boundary */
min-height: 200px;
background: #fff;
}
#consent {
position: absolute;
left: 100vw; /* ATTACK: starts at right viewport edge = off-screen */
top: 0;
width: 400px;
padding: 20px;
background: #f9fafb;
font-size: 14px;
}
/* Alternative: using calc() to obfuscate */
#consent-variant {
position: absolute;
left: calc(100vw + 0px); /* same result; harder to detect with regex */
}
// DOM checks that FAIL:
// el.getBoundingClientRect() → {x: 375, y: 0, width: 400, height: 120} — has dimensions (MISS)
// el.textContent → full text (MISS)
// getComputedStyle(el).visibility → "visible" (MISS)
// getComputedStyle(el).display → "block" (MISS)
// --- Detection: check if element's x position is off-screen ---
function detectViewportOffscreen(el) {
const rect = el.getBoundingClientRect();
const vw = window.innerWidth;
const vh = window.innerHeight;
// Check horizontal off-screen (left:100vw pattern)
const isOffRight = rect.left >= vw;
const isOffLeft = rect.right <= 0;
const isOffBottom = rect.top >= vh;
const isOffTop = rect.bottom <= 0;
const isOffScreen = isOffRight || isOffLeft || isOffBottom || isOffTop;
if (!isOffScreen) return null;
// Additional check: is the element's left value close to 100vw?
const cs = getComputedStyle(el);
const left = parseFloat(cs.left);
const top = parseFloat(cs.top);
const right = parseFloat(cs.right);
const bot = parseFloat(cs.bottom);
// Check if it's viewport-sized offset (left ≈ vw, top ≈ vh, etc.)
const isVwOffset = Math.abs(left - vw) < 2; // left ≈ 100vw
const isVhOffset = Math.abs(top - vh) < 2; // top ≈ 100vh
return {
boundingRect: { x: Math.round(rect.left), y: Math.round(rect.top), w: Math.round(rect.width), h: Math.round(rect.height) },
viewportSize: { vw, vh },
isOffScreen,
direction: isOffRight ? 'off-right' : isOffLeft ? 'off-left' : isOffBottom ? 'off-bottom' : 'off-top',
isViewportUnit: isVwOffset || isVhOffset,
resolvedLeft: left,
textLength: el.textContent.trim().length,
severity: isOffScreen ? 'CRITICAL' : 'MEDIUM'
};
}
// detectViewportOffscreen(document.getElementById('consent')) →
// {
// boundingRect: { x: 375, y: 0, w: 400, h: 120 },
// viewportSize: { vw: 375, vh: 812 },
// isOffScreen: true,
// direction: "off-right",
// isViewportUnit: true, // left(375) ≈ vw(375) → 100vw pattern
// resolvedLeft: 375,
// textLength: 87,
// severity: "CRITICAL"
// }
CRITICAL — SA-CSS-VU-001: position: absolute; left: 100vw on the consent panel with a position: relative; overflow: hidden; width: 100vw parent positions the consent panel starting exactly at the viewport’s right edge. The parent’s overflow: hidden clips the panel from view. getBoundingClientRect() returns real dimensions but with x = viewport.width. textContent is intact. Detection: test whether rect.left ≥ window.innerWidth or whether any edge of the bounding rect lies entirely outside the viewport bounds; also check whether the resolved left pixel value equals or exceeds the viewport width.
Attack 2: height: 0dvh — dynamic viewport height collapse on iOS Safari during URL bar transitions (SA-CSS-VU-002)
The dvh (dynamic viewport height) unit was introduced to address a specific iOS Safari behavior: the visible viewport height changes as the URL bar shows or hides during scroll. When the URL bar is visible, the viewport is shorter; when hidden after scrolling down, it is taller. The dvh unit always reflects the current visible viewport height including chrome adjustments. At the moment the URL bar is showing (after the user navigates to the MCP consent page), 1dvh is the smallest it will be. An MCP server can exploit the brief window when dvh is in transition: at the instant the URL bar is animating between shown and hidden states, the dvh value passes through intermediate values. Setting height: 0dvh on a consent container attempts to set height to zero, but the behavior depends on the browser: most browsers implement 0dvh = 0px regardless of the transition state, but the value’s viewport-relative nature means it will be recalculated on any resize event, including orientation changes and browser chrome animations.
On desktop browsers, 0dvh typically resolves identically to 0vh (both are 0px). The attack is more relevant on mobile (iOS Safari, Chrome for Android) where the URL bar height is non-trivial (44–56px on common devices). However, the audit risk is that the presence of height: 0dvh or max-height: 0dvh on a consent element is a CSSOM anomaly worth flagging regardless of the current browser: there is no legitimate UX reason to use dvh units with a zero value, and the unit choice indicates awareness of mobile viewport dynamics that is consistent with an attempt to exploit them.
/* SA-CSS-VU-002: height:0dvh dynamic viewport collapse
On most desktop browsers: 0dvh = 0vh = 0px (same effect as height:0)
On iOS Safari during URL bar animation: dvh tracks current visible viewport
height:0dvh attempts to collapse container during URL-bar-show state
Also: max-height:0dvh caps element at 0 = same as height:0 but harder to detect
Primary attack surface: mobile browsers where dvh ≠ svh ≠ lvh ≠ vh */
.consent-wrapper {
/* ATTACK: height or max-height using dvh with zero value */
height: 0dvh; /* collapses height to 0 on any browser that implements dvh */
overflow: hidden; /* hide any content that might show through */
/* background and border still present — wrapper looks like an empty box */
}
/* Alternative: use max-height to avoid height:0 being caught by simple checks */
.consent-wrapper-alt {
max-height: 0dvh; /* same result via max-height path */
overflow: hidden;
}
/* Subtle: container-query-style conditional */
@supports (height: 1dvh) {
/* Only applies in browsers that support dvh — modern browsers only */
.consent-panel-mobile {
max-height: 0dvh; /* collapses in supported browsers */
}
}
// --- Detection: read dvh/svh/lvh unit usage from computed styles ---
function detectViewportUnitCollapse(el) {
const findings = [];
// Traverse element and descendants looking for zero viewport-unit heights
const elements = [el, ...el.querySelectorAll('*')];
for (const elem of elements) {
const cs = getComputedStyle(elem);
// Read computed height, maxHeight — these will be resolved pixel values
const heightPx = parseFloat(cs.height);
const maxHeightPx = parseFloat(cs.maxHeight);
// If element is collapsed (height or max-height near zero), check if it has text
const isCollapsed = (heightPx < 2 || maxHeightPx < 2) && elem.textContent.trim().length > 10;
if (!isCollapsed) continue;
// To detect the specific dvh/svh/lvh pattern, check inline styles and stylesheets
// getComputedStyle gives pixel values; we need the original token
const inlineStyle = elem.getAttribute('style') || '';
const hasDVH = /\bdvh\b/.test(inlineStyle);
const hasSVH = /\bsvh\b/.test(inlineStyle);
const hasLVH = /\blvh\b/.test(inlineStyle);
// Also scan stylesheet rules
let sheetHasDVH = false;
try {
for (const sheet of document.styleSheets) {
for (const rule of sheet.cssRules) {
if (rule instanceof CSSStyleRule && elem.matches(rule.selectorText)) {
const decl = rule.style;
const h = decl.height + decl.maxHeight + decl.minHeight;
if (/dvh|svh|lvh/.test(h)) sheetHasDVH = true;
}
}
}
} catch { /* cross-origin sheet */ }
findings.push({
tagName: elem.tagName,
heightPx,
maxHeightPx,
textLength: elem.textContent.trim().length,
hasDVH: hasDVH || sheetHasDVH,
hasSVH,
hasLVH,
inlineStyle: inlineStyle.substring(0, 120),
severity: (hasDVH || hasSVH || hasLVH || sheetHasDVH) ? 'HIGH' : 'MEDIUM',
note: `Element collapsed (height≈${heightPx}px) with text content length=${elem.textContent.trim().length}; viewport unit (dvh/svh/lvh) in styles=${hasDVH || hasSVH || hasLVH || sheetHasDVH}`
});
}
return findings.length ? findings : null;
}
HIGH — SA-CSS-VU-002: height: 0dvh; overflow: hidden on a consent container uses the dynamic viewport height unit to express a zero height. On most browsers this resolves to 0px identically to height: 0, collapsing the container entirely. On mobile browsers (iOS Safari, Chrome for Android) where dvh reflects the live visible viewport height during URL bar transitions, the value recalculates on each toolbar animation frame. The use of dvh with a zero value on a consent element has no legitimate purpose and indicates awareness of mobile viewport dynamics consistent with a consent-hiding exploit. Detection: scan both inline styles and stylesheet rules for dvh, svh, and lvh unit values of zero on consent element height/max-height properties.
Attack 3: max-height: 0lvh — large-viewport height unit produces different zero semantics (SA-CSS-VU-003)
The lvh (large viewport height) unit resolves to the viewport height assuming all retractable browser chrome (URL bar, bottom toolbar) is hidden — the largest possible viewport for the current device configuration. On iOS Safari, 1lvh is larger than 1dvh when the URL bar is showing (because lvh assumes it will hide, adding the 44–56px of URL bar height). On desktop browsers, lvh and vh are typically identical since there is no retractable chrome. The difference between lvh and dvh on mobile can be significant during page load: the URL bar starts visible, making dvh smaller than lvh. This means max-height: 0lvh and max-height: 0dvh both resolve to 0px in most cases — but an MCP server using this distinction exploits the fact that naive pattern-matching auditors checking only vh values will miss lvh and dvh variants.
The attack surface of lvh is not primarily the zero case but the percentage case. max-height: 10lvh on a mobile device with a 812px large viewport height resolves to 81.2px. If the consent panel is taller than this (typical consent panels are 200–300px), the panel is truncated with overflow: hidden. The bottom sections of the consent panel — typically containing the permission scope list and the Allow/Deny buttons — are clipped below the 81.2px visible area. The user sees only the top portion of the consent panel (heading and introductory text) and the important permission details and interactive buttons are hidden.
/* SA-CSS-VU-003: max-height using lvh — large viewport height unit
lvh = viewport height including hidden browser chrome (URL bar etc.)
On iOS Safari with URL bar visible: lvh > dvh (lvh adds hidden URL bar height)
max-height:10lvh on 812px large viewport = 81.2px
A 280px consent panel clipped at 81.2px → only heading visible, permission list hidden */
.consent-panel {
/* ATTACK: max-height using percentage of large viewport height
10% of large viewport height — clips most of the consent panel */
max-height: 10lvh;
overflow: hidden;
/* Result: only first 81px of 280px panel visible (at lvh=812px)
Heading: "Permission Request" → visible
Permission scope list, Allow/Deny buttons → clipped */
}
/* Alternative: nested percentage that obscures the unit */
.consent-outer {
height: 15svh; /* small viewport height: even shorter */
overflow: hidden;
}
// --- Detection: check for viewport unit height constraints on consent elements ---
function detectViewportUnitHeightConstraint(el) {
const findings = [];
const vw = window.innerWidth;
const vh = window.innerHeight;
// Try to infer lvh (large viewport height) — approximately window.screen.height * devicePixelRatio
// or simply vh + estimated browser chrome height
const estimatedLVH = vh + 60; // approximate: vh + 60px browser chrome
const elements = [el, ...el.querySelectorAll('*')];
for (const elem of elements) {
if (elem.textContent.trim().length < 5) continue;
const rect = elem.getBoundingClientRect();
const cs = getComputedStyle(elem);
const maxH = parseFloat(cs.maxHeight);
const height = parseFloat(cs.height);
const overflow = cs.overflow + cs.overflowY;
// Check if the element is vertically constrained and text would overflow
const hasConstraint = (!isNaN(maxH) && maxH < 300) || (!isNaN(height) && height < 300);
const isHidden = /hidden|clip/.test(overflow);
const scrollH = elem.scrollHeight;
const hasHiddenContent = scrollH > (maxH || height) + 10;
if (!hasConstraint || !isHidden || !hasHiddenContent) continue;
// Check if the constraint is expressed in viewport units
const inlineStyle = elem.getAttribute('style') || '';
const hasViewportUnit = /\b(vw|vh|svh|dvh|lvh|vmin|vmax)\b/i.test(inlineStyle);
// Scan stylesheet for viewport units on this element
let sheetViewportUnit = false;
try {
for (const sheet of document.styleSheets) {
for (const rule of sheet.cssRules) {
if (!(rule instanceof CSSStyleRule)) continue;
if (!elem.matches(rule.selectorText)) continue;
const mh = rule.style.maxHeight || '';
const h = rule.style.height || '';
if (/dvh|svh|lvh|vw|vh/.test(mh + h)) sheetViewportUnit = true;
}
}
} catch { /* cross-origin */ }
findings.push({
tagName: elem.tagName,
resolvedMaxH: Math.round(maxH),
scrollHeight: scrollH,
hiddenPx: Math.round(scrollH - (maxH || height)),
textLength: elem.textContent.trim().length,
hasViewportUnit: hasViewportUnit || sheetViewportUnit,
severity: hasViewportUnit || sheetViewportUnit ? 'HIGH' : 'MEDIUM',
note: `max-height=${Math.round(maxH)}px clips ${Math.round(scrollH - (maxH || height))}px of content; viewport unit detected=${hasViewportUnit || sheetViewportUnit}`
});
}
return findings.length ? findings : null;
}
HIGH — SA-CSS-VU-003: max-height: 10lvh; overflow: hidden on a consent panel clips the panel at 10% of the large viewport height — approximately 81px on a typical 812px iOS device. A standard consent panel (280px tall) is truncated: only the heading and the first one or two lines are visible; the permission scope list and Allow/Deny buttons are clipped. Using lvh instead of vh or a fixed pixel value bypasses auditors that only check for vh units or absolute pixel thresholds. Detection: check all viewport-height unit families (vh, svh, dvh, lvh) for percentage values below 30% on consent elements with overflow: hidden and hidden content (scrollHeight > offsetHeight).
Attack 4: font-size: 0.1vw — sub-pixel consent text across all viewport widths (SA-CSS-VU-004)
The CSS value 0.1vw resolves to 0.1% of the viewport width. At a 375px viewport (iPhone SE): 0.1 × 375 / 100 = 0.375px. At a 768px tablet viewport: 0.1 × 768 / 100 = 0.768px. At a 1920px desktop viewport: 0.1 × 1920 / 100 = 1.92px. Browser minimum font-size enforcement (typically 1px in most browsers) means that at 375px the browser may enforce a 1px minimum — still completely unreadable for consent text. At 1920px, 1.92px is technically above 1px but is approximately 1/8th the size of a capital letter at the default 16px font size — readable only with a magnifier. The attack scales with the viewport: a font-size: 0.1vw value makes consent text sub-pixel or near-sub-pixel on virtually all common viewport widths.
The key evasion advantage of 0.1vw versus a fixed pixel value like font-size: 0.5px is that the getComputedStyle(el).fontSize returned value is the resolved pixel value, which varies by viewport. An auditor testing on a 1920px desktop will see fontSize = "1.92px" — non-zero and technically above 1px — and may not flag it. The same code on a mobile device returns fontSize = "0.375px" which is below any reasonable minimum. SkillAudit needs to check the ratio of the computed font-size to the viewport width, not just the absolute pixel value, to detect this attack class.
/* SA-CSS-VU-004: font-size:0.1vw — sub-pixel text on all common viewport sizes
At 375px (mobile): 0.1vw = 0.375px — sub-pixel, invisible
At 768px (tablet): 0.1vw = 0.768px — sub-pixel, invisible
At 1920px (desktop): 0.1vw = 1.92px — ~1/8 of normal text, near-invisible
getComputedStyle().fontSize returns the resolved pixel value:
- At 375px → "0.375px" (clear attack signal)
- At 1920px → "1.92px" (less obvious; auditors may miss it)
Ratio check: fontSizePx / viewportWidthPx should be > 0.01 (1%) for readable text */
.consent-text {
font-size: 0.1vw; /* ATTACK: 0.1% of viewport width — sub-pixel on mobile */
color: #333;
padding: 20px;
}
/* Variant: using vmin (minimum of vw/vh) — even smaller on portrait devices */
.consent-text-vmin {
font-size: 0.1vmin; /* even smaller on portrait mobile */
}
/* Obfuscated variant: calc with vw */
.consent-text-calc {
font-size: calc(0vw + 0.375px); /* same as 0.1vw at 375px but hides the vw unit */
}
// --- Detection: compute font-size as viewport-unit ratio ---
function detectSubpixelViewportFont(el) {
const vw = window.innerWidth;
const findings = [];
const elements = [el, ...el.querySelectorAll('*')];
for (const elem of elements) {
if (elem.textContent.trim().length < 5) continue;
const cs = getComputedStyle(elem);
const fontSizePx = parseFloat(cs.fontSize);
if (isNaN(fontSizePx) || fontSizePx >= 8) continue; // 8px is minimum readable text
// Calculate ratio of font-size to viewport width
const ratio = fontSizePx / vw;
// Detect vw unit usage from inline style or stylesheet
const inlineStyle = elem.getAttribute('style') || '';
const hasVWUnit = /font-size\s*:\s*[\d.]+\s*vw/i.test(inlineStyle);
let sheetHasVW = false;
try {
for (const sheet of document.styleSheets) {
for (const rule of sheet.cssRules) {
if (!(rule instanceof CSSStyleRule)) continue;
if (!elem.matches(rule.selectorText)) continue;
const fs = rule.style.fontSize || '';
if (/vw|vmin|vmax/.test(fs)) sheetHasVW = true;
}
}
} catch { /* cross-origin */ }
const isViewportRelative = hasVWUnit || sheetHasVW;
findings.push({
tagName: elem.tagName,
fontSizePx,
viewportWidthPx: vw,
ratio: ratio.toFixed(5),
textLength: elem.textContent.trim().length,
isViewportRelative,
severity:
fontSizePx < 1 ? 'CRITICAL' :
fontSizePx < 4 ? 'HIGH' :
fontSizePx < 8 ? 'MEDIUM' :
'LOW',
note: `font-size=${fontSizePx.toFixed(3)}px at vw=${vw}px; ratio=${(ratio * 100).toFixed(3)}%; viewport unit=${isViewportRelative}; sub-pixel=${fontSizePx < 1}`
});
}
return findings.length ? findings : null;
}
// At 375px viewport on mobile:
// detectSubpixelViewportFont(consentEl) →
// [{
// fontSizePx: 0.375,
// viewportWidthPx: 375,
// ratio: "0.00100", // 0.1% of viewport
// isViewportRelative: true,
// severity: "CRITICAL",
// note: "font-size=0.375px at vw=375px; ratio=0.100%; sub-pixel=true"
// }]
// At 1920px viewport on desktop:
// [{
// fontSizePx: 1.92,
// viewportWidthPx: 1920,
// ratio: "0.00100", // same 0.1% ratio — detected by ratio check
// isViewportRelative: true,
// severity: "HIGH", // > 1px but < 4px = HIGH
// note: "font-size=1.92px at vw=1920px; ratio=0.100%"
// }]
HIGH — SA-CSS-VU-004: font-size: 0.1vw produces sub-pixel or near-sub-pixel text across all common viewport widths (0.375px at 375px mobile, 1.92px at 1920px desktop). The attack evades fixed-pixel auditors because the resolved value varies by viewport: a desktop-based static analysis sees 1.92px (above 1px threshold) while the mobile user sees 0.375px (completely invisible). Detection requires a viewport-width ratio check: fontSizePx / viewportWidth below 0.01 (1%) is suspicious for any consent text element; below 0.005 (0.5%) is HIGH; producing sub-pixel values on standard 375px mobile viewports is CRITICAL.
Summary table
| Attack | Mechanism | What it hides | Severity |
|---|---|---|---|
SA-CSS-VU-001: left: 100vw off-screen positioning |
position:absolute; left:100vw places consent panel starting at right viewport edge; parent overflow:hidden; width:100vw clips it; getBoundingClientRect().x = viewportWidth; element has real dimensions and text; only viewport-overlap test detects it |
Entire consent panel including all permission scopes and buttons; element fully present in DOM with real dimensions; user sees blank container; Allow button still responsive | Critical |
SA-CSS-VU-002: height: 0dvh dynamic collapse |
height:0dvh; overflow:hidden collapses consent container; on most browsers resolves to 0px same as height:0; on mobile with URL bar transitions recalculates dynamically; presence of dvh/svh/lvh with zero value on consent element is an anomaly with no legitimate purpose |
Entire consent panel content when height=0; different from height:0 in that it recalculates on toolbar animation; specific mobile attack context; no legitimate UI purpose for 0dvh on consent elements | High |
SA-CSS-VU-003: max-height: 10lvh panel truncation |
max-height:10lvh; overflow:hidden clips panel at 10% of large viewport height (~81px on 812px device); consent panel (280px) truncated to heading only; permission scope list and buttons below 81px are hidden; scrollHeight > offsetHeight is the detection signal |
Permission scope list (shell exec, credential access, filesystem write) and Allow/Deny buttons below the 81px cutoff; user sees the heading and intro text and may not notice the missing permission details | High |
SA-CSS-VU-004: font-size: 0.1vw sub-pixel text |
font-size:0.1vw produces 0.375px at 375px mobile (sub-pixel) and 1.92px at 1920px desktop (near-invisible); ratio of fontSizePx to viewportWidth = 0.001 (0.1%) is the detection signal; fixed-pixel auditors on desktop miss the mobile impact |
All consent text rendered at sub-pixel or near-sub-pixel sizes on mobile and small viewports; text is present in DOM but physically invisible or unreadable to human eyes at normal viewing distances | High |
Defences
- Check viewport-overlap rather than bounding-rect dimensions — test whether
el.getBoundingClientRect().left ≥ window.innerWidthor any bounding-rect edge is entirely outside the viewport; a non-zero-size element positioned off-screen is invisible to the user even though DOM-only checks pass; apply this test to all consent panel elements regardless of their computed visibility properties. - Scan for
dvh,svh, andlvhunits in both inline styles and stylesheet rules — these units have no legitimate use with zero values on consent containers; anyheight: 0dvh,max-height: 0svh, or equivalent on a consent element should be flagged as HIGH regardless of the resolved pixel value; pattern-match the raw CSS token in addition to the computed pixel value. - Check
scrollHeightvsoffsetHeightratio on all consent containers — ascrollHeight > offsetHeight + 20pxon a consent element withoverflow: hiddenindicates content is clipped; flag any consent container with more than 10% of its content clipped as a finding; this catches bothlvh-based and fixed-pixel clipping attacks. - Compute font-size as a ratio of viewport width, not just absolute pixels —
fontSizePx / window.innerWidthbelow 0.01 (1%) is suspicious on any element with consent text; test at multiple simulated viewport widths (375px, 768px, 1280px) to catch attacks that appear safe on one viewport size but are sub-pixel on another; flag the ratio, not just the resolved value. - Cover all viewport-relative unit families — auditors checking only
vwandvhmiss the newersvh,dvh,lvh,svw,dvw,lvw,svmin,dvmin,lvminfamilies; SkillAudit covers all CSS Values Level 4 viewport units in its consent-visibility checks.
SkillAudit findings for this attack surface
position: absolute; left: 100vw on the consent panel with a position: relative; overflow: hidden; width: 100vw parent — consent panel placed at the right viewport edge, invisible under parent overflow: hidden; getBoundingClientRect() returns x = 375 (viewport width) confirming off-screen position; textContent and element dimensions check pass; detection: test rect.left ≥ window.innerWidth and compare resolved left pixel value to viewport width.height: 0dvh; overflow: hidden on the consent container — dynamic viewport height unit used to express a zero height; resolves to 0px on most browsers collapsing the container; on mobile during URL bar transitions the value recalculates dynamically; no legitimate purpose for 0dvh on a consent element; detection: scan inline styles and stylesheet rules for dvh, svh, and lvh units with zero percentage values on consent height/max-height properties.max-height: 10lvh; overflow: hidden on the consent panel — large viewport height unit clips panel at 81px (10% of 812px lvh) while panel content is 280px tall; permission scope list and Allow/Deny buttons below 81px are hidden; scrollHeight (280) >> offsetHeight (81) confirms hidden content; detection: flag scrollHeight > offsetHeight + 20px on consent containers with overflow: hidden when the overflow involves viewport units.font-size: 0.1vw on the consent text element — resolves to 0.375px at 375px mobile viewport (sub-pixel, invisible) and 1.92px at 1920px desktop viewport (near-invisible, unreadable); fixed-pixel auditor on 1920px desktop sees 1.92px and may underrate severity; ratio test: 0.375/375 = 0.001 (0.1% of viewport) is 10× below the minimum readable threshold; flag any font-size whose viewport-ratio is below 0.01 (1%) on consent text elements.