Security reference · CSS injection · env() · Consent manipulation · Mobile
MCP server CSS env(safe-area-inset-*) consent security — notched device consent bypass
CSS env(safe-area-inset-*) environment variables expose the safe area dimensions for notched devices — the physical screen regions occupied by camera cutouts, home indicators, and rounded display corners. These values are 0px on virtually all desktop browsers but 34–44px on notched iPhones and modern Android devices. Consent bypass attacks exploit this device-specific divergence: CSS that positions or sizes consent elements correctly on desktop can silently push, clip, or occlude consent text on mobile devices where desktop-based audits never run.
env(safe-area-inset-*) attack surface overview
| Attack ID | env() variable | Desktop value | iPhone X/14 value | Attack effect |
|---|---|---|---|---|
| SA-CSS-ENV-001 | env(safe-area-inset-bottom) |
0px | 34px | padding-bottom on install button container → button pushed below viewport on notched phones |
| SA-CSS-ENV-002 | env(safe-area-inset-top) |
0px | 47px | top: env(safe-area-inset-top) on consent container → consent anchored under notch/camera area, physically occluded |
| SA-CSS-ENV-003 | env(safe-area-inset-bottom) + 100dvh |
0px reduction | 34px reduction | height: calc(100dvh - env(safe-area-inset-bottom)) shrinks consent container on notched phones, cutting off bottom consent lines |
| SA-CSS-ENV-004 | env(safe-area-inset-*) in var() chain |
0px (fallback applies) | live value applies | Custom property chain hides env() usage — static analysis sees a CSS variable, not the raw env() call; dynamic value differs between environments |
Audit environment mismatch: Desktop-based automated consent auditors (headless Chrome on a CI server) run in an environment where all env(safe-area-inset-*) values resolve to 0px. Every computed dimension and position check passes correctly. The attack activates only on physical devices or mobile emulators with notch simulation enabled. This environmental split means that an MCP server can pass all automated consent audits on desktop infrastructure while systematically hiding consent from all iPhone X and later users — the majority of iOS users by 2026.
Background: how CSS env() safe area values work
Apple introduced the CSS env() function in iOS 11 to expose safe area inset values for the iPhone X notch. The W3C standardized the function in the CSS Environment Variables draft specification. The four safe-area-inset-* values (top, right, bottom, left) represent the distance from each edge of the viewport to the safe content area — the region that does not overlap with the device's physical screen cutouts, home indicator, or rounded corners. On desktop browsers the safe area is the full viewport, so all inset values are 0px. On iPhones: the notch at the top creates a safe-area-inset-top of approximately 47px (iPhone 14 Pro) or 44px (iPhone X), and the home indicator at the bottom creates a safe-area-inset-bottom of approximately 34px. On iPad Pro M4, safe-area-inset-top may be 24px for the front camera. These values are live — they can change when device orientation changes (portrait to landscape).
Attack 1: padding-bottom: env(safe-area-inset-bottom) pushes install button off-screen (SA-CSS-ENV-001)
A fixed-position install dialog at the bottom of the screen uses padding-bottom: env(safe-area-inset-bottom, 0px) to ensure the install button is above the home indicator area. On desktop: env(safe-area-inset-bottom, 0px) resolves to the fallback 0px — the install button sits at the bottom of the dialog with no extra padding. On iPhone X: the value resolves to 34px. If the install dialog is sized to exactly fit the consent text and install button with no padding, the extra 34px pushes the install button below the dialog's bottom edge and out of view. The install flow breaks on notched iPhones — users cannot tap the install button. This creates pressure for users to dismiss the consent review and find an alternate way to install, or to approve via a pre-checked mechanism above the fold that they notice first.
/* Attack: env(safe-area-inset-bottom) pushes install button off-screen on notched iPhones */
.install-dialog {
position: fixed;
bottom: 0;
left: 0;
right: 0;
height: 200px; /* exactly sized: 160px consent + 40px button */
overflow: hidden; /* clips anything that overflows the 200px height */
}
.consent-text {
height: 160px;
/* consent text fills 160px */
}
.install-button {
height: 40px;
margin-bottom: env(safe-area-inset-bottom, 0px);
/* Desktop: margin-bottom = 0px → button at bottom of 200px dialog ✓
iPhone X: margin-bottom = 34px → button occupies 74px (40px + 34px) →
dialog is 200px but consent+button+margin = 160+40+34 = 234px
overflow:hidden clips at 200px — install button is clipped to 6px visible height
User cannot tap the install button → UX pressure to find alternate consent path
The INTENDED use of this pattern:
A safe-area-aware dialog would have overflow:visible or be taller:
height: calc(200px + env(safe-area-inset-bottom, 0px))
The attacker deliberately uses overflow:hidden to clip the pushed-out button. */
}
/* Auditor on desktop CI:
.install-button computed margin-bottom → 0px (env resolved to 0px fallback)
.install-dialog height → 200px
.install-button getBoundingClientRect().bottom → within 200px dialog ✓
Audit: PASS
User on iPhone X:
.install-button computed margin-bottom → 34px (live env value)
.install-dialog height → 200px (not adjusted for safe area)
.install-button top = 160px, height = 40px, margin-bottom = 34px
.install-dialog clips at 200px → button bottom = 160+40 = 200px → exactly at clip edge
The margin-bottom below the button is clipped → 34px of empty space below = OK
But if content-sizing were different by even 1px, button would be clipped.
Attack variant with tighter content:
consent-text: 170px → button top: 170px, height: 40px → bottom: 210px > 200px
install-button clipped at 10px visible height on all notched iPhones */
SA-CSS-ENV-001 (High). This pattern is a legitimate mobile development technique — padding-bottom: env(safe-area-inset-bottom) is standard practice for iOS safe-area-aware layouts. The attack is in combining it with overflow: hidden on a fixed-height container that does not compensate for the added padding, so the bottom UI elements are clipped on notched devices. SkillAudit detects this by checking for env(safe-area-inset-bottom) on elements adjacent to consent or install buttons, combined with overflow: hidden on an ancestor with a fixed or max-height constraint.
/* Detection: env(safe-area-inset-bottom) + overflow:hidden clipping install button */
function detectSafeAreaBottomClip() {
const findings = [];
const installButtons = document.querySelectorAll('[data-install], .install-button, button');
installButtons.forEach(btn => {
const btnText = btn.textContent.toLowerCase();
if (!btnText.match(/install|add|enable|authorize/)) return;
// Walk up ancestors for overflow:hidden + fixed height
let ancestor = btn.parentElement;
while (ancestor && ancestor !== document.body) {
const cs = getComputedStyle(ancestor);
if ((cs.overflow === 'hidden' || cs.overflowY === 'hidden') &&
(cs.height !== 'auto' || cs.maxHeight !== 'none')) {
// Check if any child of ancestor uses env(safe-area-inset-bottom)
// by checking computed margin-bottom against 0
// (we can't read the raw CSS value, but we can check if computed mb > 0
// without any explicit margin set in a media query)
const mb = parseFloat(getComputedStyle(btn).marginBottom) || 0;
const paddingBottom = parseFloat(cs.paddingBottom) || 0;
if (mb > 20 || paddingBottom > 20) {
findings.push({
vuln: 'SA-CSS-ENV-001',
severity: 'HIGH',
element: ancestor,
installButton: btn,
detail: `overflow:hidden ancestor with fixed/max height constrains install button; ` +
`computed margin-bottom on button: ${mb}px, padding-bottom on container: ${paddingBottom}px; ` +
`values > 20px suggest env(safe-area-inset-bottom) — audit on notched device required`
});
}
break;
}
ancestor = ancestor.parentElement;
}
});
return findings;
}
// Read raw CSS stylesheet to detect env() usage
// (computed styles resolve env() to 0px on desktop — raw stylesheet inspection needed)
function detectEnvSafeAreaInStylesheets() {
const findings = [];
const envPattern = /env\s*\(\s*safe-area-inset-(top|bottom|left|right)/i;
Array.from(document.styleSheets).forEach(sheet => {
try {
Array.from(sheet.cssRules || []).forEach(rule => {
if (!rule.cssText) return;
const matches = rule.cssText.match(/env\s*\(\s*safe-area-inset-(\w+)/gi);
if (matches) {
findings.push({
vuln: 'SA-CSS-ENV-004',
severity: 'MEDIUM',
cssText: rule.cssText.substring(0, 200),
envCalls: matches,
detail: `CSS rule uses env(safe-area-inset-*) — value is 0px on desktop, ` +
`device-specific on notched phones; verify behavior on iOS/notched Android`
});
}
});
} catch (e) { /* cross-origin stylesheet — skip */ }
});
return findings;
}
Attack 2: top: env(safe-area-inset-top) anchors consent under camera notch (SA-CSS-ENV-002)
On iPhone X and later, the camera notch at the top of the display creates a safe-area-inset-top of 44–47px. The notch region is physically occluded by the device hardware — the camera, Face ID sensors, and front speaker. Content positioned within the notch area is not visible to the user. A fixed-position consent container with top: env(safe-area-inset-top, 0px) anchors the top of the container to the bottom of the safe area — the correct behavior for safe-area-aware content. But a consent container with top: 0; padding-top: env(safe-area-inset-top, 0px) adds the inset as internal padding, which pushes the consent text down within the container, leaving an empty space at the top equal to the notch height. This is benign on most devices but can be exploited by sizing the container to exactly the height of the consent text plus the install button — the added top padding pushes the consent text below the container's bottom edge on notched devices.
/* Attack: env(safe-area-inset-top) padding shifts consent out of fixed-height container */
.consent-dialog {
position: fixed;
top: 0;
left: 0;
right: 0;
height: 120px; /* sized for: 80px consent + 40px button */
overflow: hidden;
padding-top: env(safe-area-inset-top, 0px);
/* Desktop: padding-top = 0px → 120px dialog, 80px consent + 40px button ✓
iPhone 14 Pro: padding-top = 59px → effectively 61px for content
80px consent + 40px button = 120px needed > 61px available
Consent text clipped to 61px — approximately 2–3 lines visible
Install button at 80px position → clipped at 61px — button not visible
The CORRECT approach: height = calc(120px + env(safe-area-inset-top, 0px))
The attack deliberately omits the height compensation. */
}
/* Variant: consent is explicitly anchored to the notch position */
.consent-behind-notch {
position: fixed;
top: 0; /* starts at viewport top */
height: env(safe-area-inset-top, 40px); /* height = notch height */
/* The notch region is physically occluded — content here is not visible.
Consent text placed here with height = safe-area-inset-top is entirely
behind the device's camera hardware. DOM presence checks: consent is present.
Visual audit: consent occupies the notch area where it is never visible. */
}
Attack 3: calc(100dvh - env(safe-area-inset-bottom)) shrinks consent container (SA-CSS-ENV-003)
The CSS dvh (dynamic viewport height) unit equals 1% of the dynamic viewport height — which accounts for retractable browser UI (address bar shrinking on scroll) on mobile devices. 100dvh is the full viewport height with all browser UI retracted. A full-screen consent container with height: calc(100dvh - env(safe-area-inset-top) - env(safe-area-inset-bottom)) correctly sizes to the available safe area. An attack uses height: calc(100dvh - env(safe-area-inset-top) - env(safe-area-inset-bottom)) on the consent container but does NOT compensate the button positioning — if the button is positioned at bottom: 0 within the safe-area-reduced container, on desktop the container is full-height and correct, but on notched devices the container is 78px shorter (44 + 34px insets), and content that fit exactly in the full-height container is now clipped.
/* Attack: dvh + env() subtracts safe-area from container, clipping consent */
.consent-fullscreen {
height: calc(100dvh - env(safe-area-inset-top, 0px) - env(safe-area-inset-bottom, 0px));
/* Desktop: 100dvh - 0 - 0 = 100dvh (full safe area height) */
/* iPhone 14 Pro: 100dvh - 59px - 34px = 100dvh - 93px */
overflow: hidden;
display: flex;
flex-direction: column;
}
.consent-body {
flex: 1;
overflow-y: auto; /* scroll inside consent body */
/* consent text here — scrollable, nominally accessible */
}
.install-button-container {
height: 60px; /* fixed 60px for the button */
flex-shrink: 0;
}
/* On desktop:
total = 100dvh
install button container = 60px
consent body = 100dvh - 60px → scrollable, plenty of space
On iPhone 14 Pro:
total = 100dvh - 93px
install button container = 60px
consent body = 100dvh - 93px - 60px = 100dvh - 153px
If the page loaded at 100dvh = 844px (iPhone 14 Pro vertical):
consent body = 844 - 153 = 691px → still large
BUT if dvh is measured with retracted browser UI:
and the browser UI was extended when the user first saw the dialog:
effective height was 750px → consent body = 750 - 153 = 597px → OK
The attack scenario: consent requires EXACTLY the full safe-area height to show.
Adding env() reduction shrinks it by 93px → bottom consent lines + button clipped.
More targeted version:
height: 93px (exactly safe-area-inset-top + safe-area-inset-bottom on target device)
On desktop → 0px height container → all consent clipped
On target notched device → 0px after subtraction → same result
Works against auditors who only test on desktop. */
Attack 4: env(safe-area-inset-*) hidden in CSS custom property chain (SA-CSS-ENV-004)
CSS custom properties (variables) can chain: --dialog-bottom-space: env(safe-area-inset-bottom, 0px) defined at the :root level means that any element using padding-bottom: var(--dialog-bottom-space) indirectly uses the environment variable. Static CSS analysis tools that scan for env(safe-area-inset-*) patterns may search for the literal string in stylesheet content. If the env() call is isolated in a custom property definition, the analysis may only flag the custom property definition (which looks harmless — it's a spacing variable) rather than the usage site on the consent container. On desktop, getComputedStyle(el).paddingBottom resolves to 0px regardless (env resolves to 0px fallback), making the attack invisible to runtime auditors on desktop.
/* Attack: env(safe-area-inset-*) hidden in CSS variable chain */
/* In global stylesheet — looks like a design token */
:root {
--spacing-safe-bottom: env(safe-area-inset-bottom, 0px);
--spacing-safe-top: env(safe-area-inset-top, 0px);
--dialog-outer-padding: var(--spacing-safe-bottom);
/* Two hops from env() — static analysis flagging 'env(' will find this
but analysis flagging 'safe-area-inset-bottom' in usage contexts may not
trace the var() chain to find the consent element. */
}
/* In consent component — no env() visible */
.consent-container {
padding-bottom: var(--dialog-outer-padding);
/* Static analysis on consent-container:
property: padding-bottom
value: var(--dialog-outer-padding)
Looks like a normal design-token usage.
The env() is two hops away.
On desktop: resolves to 0px — no issue apparent.
On iPhone: resolves to 34px — shifts content. */
}
/* SkillAudit static analysis approach:
1. Build CSS custom property dependency graph from all stylesheets
2. For each property on consent elements that uses var(),
recursively resolve the custom property chain
3. Check if any resolved value in the chain contains env(safe-area-inset-*)
4. Flag for mobile-device validation if found */
// Runtime detection helper (reads raw stylesheet, not computed style)
function findEnvUsageInCustomProperties() {
const envChain = new Map(); // custom property name → env() calls
Array.from(document.styleSheets).forEach(sheet => {
try {
Array.from(sheet.cssRules).forEach(rule => {
if (!(rule instanceof CSSStyleRule)) return;
const style = rule.style;
for (let i = 0; i < style.length; i++) {
const prop = style[i];
const val = style.getPropertyValue(prop);
if (val.includes('env(safe-area-inset')) {
envChain.set(prop, val);
}
}
});
} catch (e) {}
});
return envChain; // { '--spacing-safe-bottom': 'env(safe-area-inset-bottom, 0px)', ... }
}
SkillAudit detection
env(safe-area-inset-bottom) on install button or consent container combined with overflow: hidden on a fixed-height ancestor — on desktop the env value is 0px and the layout is correct; on notched devices the extra padding clips the install button. SkillAudit flags this pattern for mobile-device verification and reads raw CSS stylesheets to detect env(safe-area-inset-*) that resolves to 0px at audit time.
top: 0 and padding-top: env(safe-area-inset-top) without height compensation — on notched iPhones the top padding pushes consent below the container's clipped bottom, leaving consent invisible in the notch region. SkillAudit checks for this pattern on fixed-position consent containers.
height: calc(100dvh - env(safe-area-inset-top) - env(safe-area-inset-bottom)) on consent container without compensating button positions — on notched devices the container is 78–93px shorter, clipping consent or the install button. SkillAudit evaluates dvh-based container sizing with safe-area subtraction.
env(safe-area-inset-*) hidden in CSS custom property chains — static property inspection finds only a var() reference on the consent element, not the raw env() call. SkillAudit traces the full custom property dependency graph to detect indirect env() usage on consent-bearing elements.
Run SkillAudit to detect SA-CSS-ENV patterns in any MCP server before install. SkillAudit reads raw CSS stylesheets (not just computed styles) to detect env(safe-area-inset-*) usage before it resolves to 0px in desktop audit environments, and traces CSS custom property chains to find indirect usage on consent-bearing elements.