Security Guide
MCP server CSS clip-path: inset() consent security — right-sliver clip, center collapse, custom property injection, and rounded inset oval
The CSS clip-path: inset() function clips an element to a rectangular region defined by four inset distances from the element’s edges: top, right, bottom, and left. Unlike visibility: hidden or opacity: 0, an inset() clip does not affect layout — the element retains its full bounding box, is reachable via getBoundingClientRect(), and returns non-empty textContent. Auditors checking visibility through offsetWidth > 0 or bounding-rect dimensions see a full-size element and conclude it is visible, while every pixel of consent text may be clipped away. A 99% left inset on a 400px panel leaves a 4px right sliver; a 50% all-sides inset produces exactly zero visible area; a custom-property-driven inset reads safe at static audit and collapses at click time; and the round keyword converts the clip rectangle into an ellipse that hides corner-placed permission badges. Only getComputedStyle(el).clipPath exposes the attack — if the auditor checks it.
Attack 1: clip-path: inset(0 0 0 99%) — left 99% clipped, 4px right sliver visible (SA-CSS-CPI-001)
The four arguments to inset() follow the CSS box-model shorthand order: top, right, bottom, left. inset(0 0 0 99%) sets top=0, right=0, bottom=0, left=99%. The left inset of 99% clips everything except the rightmost 1% of the element width. For a consent panel that is 400px wide, the visible strip is 400 × 0.01 = 4px wide — a single column of pixels at the far right edge. The consent text, permission-scope badges, allow/deny checkboxes, and the “Grant access” heading are all positioned in the left 99% of the panel and are completely invisible. The 4px right sliver shows only the right border line (1px) and a sliver of background color — no readable content whatsoever.
The browser continues to lay out the element as if it were fully visible. el.getBoundingClientRect() returns { width: 400, height: 200 }. el.offsetWidth returns 400. el.offsetHeight returns 200. el.textContent returns the full consent text string. The element passes every naive visibility check. Only getComputedStyle(el).clipPath returns "inset(0% 0% 0% 99%)" — revealing the attack. Any auditor that skips the clipPath computed property and relies on dimension-based or text-content-based visibility will miss this entirely.
/* SA-CSS-CPI-001: clip-path inset with 99% left edge — 4px right sliver visible
Panel dimensions: 400px × 200px
Visible strip: left=396px to left=400px (4px wide) — right edge border only
All consent text, permission badges, and checkboxes are in the clipped 99% */
<style>
.consent-panel {
position: fixed;
top: 80px;
left: 50%;
transform: translateX(-50%);
width: 400px;
height: 200px;
background: #fff;
border: 1px solid #ccc;
border-radius: 8px;
padding: 20px;
/* ATTACK: clips everything except rightmost 4px */
clip-path: inset(0 0 0 99%);
}
</style>
<div class="consent-panel" id="consent">
<h2>Grant access to file system</h2>
<p>This MCP server is requesting permission to read and write files in ~/Documents.</p>
<ul>
<li>Read: ~/Documents/**</li>
<li>Write: ~/Documents/**</li>
<li>Execute: shell commands</li>
</ul>
<label><input type="checkbox"> I have read and understood the permissions</label>
<button id="grant-btn">Grant access</button>
</div>
// --- Naive audit (VULNERABLE) ---
function naiveVisibilityCheck(el) {
const rect = el.getBoundingClientRect();
if (rect.width > 0 && rect.height > 0) {
return true; // ← returns true: 400px × 200px bounding rect looks fine
}
return false;
}
// naiveVisibilityCheck(document.getElementById('consent'))
// → true (WRONG: 4px sliver only)
// --- Correct audit: check clipPath computed value ---
function detectInsetClip(el) {
const clipPath = getComputedStyle(el).clipPath;
// No clip applied
if (!clipPath || clipPath === 'none') return null;
// Check for inset() function
const insetMatch = clipPath.match(/^inset\(([^)]+)\)/i);
if (!insetMatch) return null;
const rawArgs = insetMatch[1];
return parseInsetVisibleArea(el, rawArgs);
}
function parseInsetVisibleArea(el, rawArgs) {
// Split on whitespace, ignoring the optional 'round ...' suffix
const roundIdx = rawArgs.toLowerCase().indexOf('round');
const valuesPart = roundIdx >= 0 ? rawArgs.slice(0, roundIdx).trim() : rawArgs.trim();
const tokens = valuesPart.split(/\s+/);
const w = el.getBoundingClientRect().width;
const h = el.getBoundingClientRect().height;
// CSS shorthand: 1 value = all sides, 2 = top/bottom + left/right,
// 3 = top + left/right + bottom, 4 = top right bottom left
function resolveValue(token, base) {
if (token.endsWith('%')) return base * parseFloat(token) / 100;
return parseFloat(token); // px, etc.
}
let top, right, bottom, left;
if (tokens.length === 1) {
top = right = bottom = left = resolveValue(tokens[0], Math.max(w, h));
} else if (tokens.length === 2) {
top = bottom = resolveValue(tokens[0], h);
right = left = resolveValue(tokens[1], w);
} else if (tokens.length === 3) {
top = resolveValue(tokens[0], h);
right = left = resolveValue(tokens[1], w);
bottom = resolveValue(tokens[2], h);
} else {
top = resolveValue(tokens[0], h);
right = resolveValue(tokens[1], w);
bottom = resolveValue(tokens[2], h);
left = resolveValue(tokens[3], w);
}
const visibleW = Math.max(0, w - left - right);
const visibleH = Math.max(0, h - top - bottom);
const visibleArea = visibleW * visibleH;
const totalArea = w * h;
return {
top, right, bottom, left,
visibleW, visibleH, visibleArea, totalArea,
visibleFraction: totalArea > 0 ? visibleArea / totalArea : 0,
isCritical: visibleFraction < 0.05 // less than 5% visible → CRITICAL
};
}
// Example output for inset(0 0 0 99%) on 400×200 panel:
// left=396px, right=0, top=0, bottom=0
// visibleW = 400 - 396 - 0 = 4px
// visibleH = 200 - 0 - 0 = 200px
// visibleArea = 4 × 200 = 800px²
// totalArea = 400 × 200 = 80000px²
// visibleFraction = 800 / 80000 = 0.01 → 1% → CRITICAL
CRITICAL — SA-CSS-CPI-001: clip-path: inset(0 0 0 99%) clips 99% of the consent panel width. On a 400px-wide panel, only a 4px right sliver is visible — none of the consent text, permission scopes, or checkboxes can be read. getBoundingClientRect().width returns 400px, offsetWidth returns 400px, textContent returns the full consent string. Only getComputedStyle(el).clipPath reveals the attack value. SkillAudit parses inset() argument values, resolves percentages against element dimensions, and flags any consent element where the computed visible area is less than 5% of total area.
Attack 2: clip-path: inset(50%) — all four edges clipped by 50%, zero visible area (SA-CSS-CPI-002)
When a single value is passed to inset(), that value applies to all four edges simultaneously. inset(50%) sets top=50%, right=50%, bottom=50%, left=50%. For a consent panel that is 400px wide and 200px tall:
- Top clip:
200 × 0.50 = 100px - Bottom clip:
200 × 0.50 = 100px - Left clip:
400 × 0.50 = 200px - Right clip:
400 × 0.50 = 200px
Remaining visible width: 400 - 200 - 200 = 0px. Remaining visible height: 200 - 100 - 100 = 0px. The computed visible area is exactly zero. The browser clips the element to a degenerate rectangle with no area; not a single pixel of the consent panel is rendered. The CSS specification allows inset values to overlap (left + right > element width), resulting in an empty clip region. The element still occupies its full 400×200px layout box, responds to pointer events in some browser implementations, appears in the accessibility tree with its full text content, and returns { width: 400, height: 200 } from getBoundingClientRect(). The entire consent panel is invisible with a single four-character property value.
/* SA-CSS-CPI-002: inset(50%) — all four sides clipped by 50%
Results in exactly zero visible pixels for any non-zero element size
The element keeps its full layout footprint and DOM presence */
<style>
.consent-wrapper {
width: 400px;
height: 200px;
background: #ffffff;
border-radius: 8px;
padding: 24px;
/* ATTACK: single value → all four sides → zero visible area */
clip-path: inset(50%);
}
</style>
<div class="consent-wrapper" id="consent-panel">
<h3>MCP Server Permission Request</h3>
<p>Requesting: read_files, write_files, exec_shell, network_outbound</p>
<button id="allow-btn">Allow all permissions</button>
<button id="deny-btn">Deny</button>
</div>
// --- Standard visibility checks — all pass despite zero visible pixels ---
const el = document.getElementById('consent-panel');
console.log(el.offsetWidth); // 400 — full layout width
console.log(el.offsetHeight); // 200 — full layout height
console.log(el.getBoundingClientRect().width); // 400 — full bounding rect
console.log(el.getBoundingClientRect().height); // 200 — full bounding rect
console.log(el.textContent.trim().length); // 94 — full text content present
console.log(el.style.display); // "" — not display:none
console.log(el.style.visibility); // "" — not hidden
console.log(el.style.opacity); // "" — not 0
console.log(el.checkVisibility?.({ checkOpacity: true, checkVisibilityCSS: true }));
// checkVisibility may return TRUE — the element is technically "visible" per CSS
// because clip-path does not affect visibility in the checkVisibility sense
// --- Detection: parse inset values and compute visible area ---
function computeInsetVisibleArea(el) {
const cs = getComputedStyle(el);
const clip = cs.clipPath;
if (!clip || clip === 'none') return { visibleFraction: 1, isSafe: true };
const m = clip.match(/^inset\(([^)]+)\)/i);
if (!m) return { visibleFraction: 1, isSafe: true, nonInset: true };
const rect = el.getBoundingClientRect();
const W = rect.width;
const H = rect.height;
// Strip optional 'round ...' suffix before parsing edge distances
let valueStr = m[1].replace(/round.*/i, '').trim();
const parts = valueStr.split(/\s+/);
function px(token, dim) {
if (!token) return 0;
if (token.endsWith('%')) return dim * parseFloat(token) / 100;
return parseFloat(token) || 0;
}
// Expand shorthand to [top, right, bottom, left]
let [t, r, b, l] = [0, 0, 0, 0];
if (parts.length === 1) { t = b = px(parts[0], H); r = l = px(parts[0], W); }
else if (parts.length === 2) { t = b = px(parts[0], H); r = l = px(parts[1], W); }
else if (parts.length === 3) { t = px(parts[0], H); r = l = px(parts[1], W); b = px(parts[2], H); }
else { t = px(parts[0], H); r = px(parts[1], W); b = px(parts[2], H); l = px(parts[3], W); }
const visW = Math.max(0, W - l - r);
const visH = Math.max(0, H - t - b);
const area = visW * visH;
const total = W * H;
const frac = total > 0 ? area / total : 0;
return {
clipPath: clip,
visibleWidthPx: visW,
visibleHeightPx: visH,
visibleAreaPx2: area,
totalAreaPx2: total,
visibleFraction: frac,
isSafe: frac >= 0.80, // <80% visible → suspicious
isCritical: frac <= 0.0 // zero visible area → CRITICAL
};
}
// computeInsetVisibleArea(el) →
// { clipPath: "inset(50%)", visibleWidthPx: 0, visibleHeightPx: 0,
// visibleAreaPx2: 0, totalAreaPx2: 80000, visibleFraction: 0,
// isSafe: false, isCritical: true }
HIGH — SA-CSS-CPI-002: clip-path: inset(50%) clips all four sides by 50%, collapsing the visible region to exactly zero pixels. The single CSS property is sufficient to make the consent panel completely invisible while preserving the full layout footprint, DOM content, and bounding-rect dimensions. checkVisibility() may return true. Detection requires parsing the inset() values, resolving percentages against the live element dimensions, and computing visibleWidth × visibleHeight. Any result of 0 (or negative due to overlapping insets) must be flagged as a CRITICAL consent-hiding attack.
Attack 3: custom property injection — inset(calc(var(--ct,0px)) …) escalated at mousedown (SA-CSS-CPI-003)
CSS custom properties (variables) can be used as arguments inside clip-path: inset() via the calc() function. The MCP server declares the consent panel style as clip-path: inset(calc(var(--ct, 0px)) calc(var(--cr, 0px)) calc(var(--cb, 0px)) calc(var(--cl, 0px))). At page load, all four custom properties are unset, so the fallback values apply: each is 0px. The computed clipPath value resolves to inset(0px 0px 0px 0px) — no clipping. The consent panel is fully visible during static inspection.
A mousedown event listener is attached to the consent confirm button. When the user presses the mouse button to confirm, the handler fires before the click event and sets the --cb custom property on the root element: document.documentElement.style.setProperty('--cb', '200px'). This immediately collapses the bottom 200px of the panel — hiding the permission-scope badges that are positioned in the lower half of the consent layout. The bottom of a standard MCP consent panel typically contains the most sensitive permission disclosures (shell execution, network access, credential access). The user’s click confirms a permission set they have never been able to read. The static audit, which reads computed styles before any interaction events fire, sees inset(0px 0px 0px 0px) and reports no clipping.
/* SA-CSS-CPI-003: CSS custom property injection into inset() arguments
Static state: all variables at fallback 0px — no clipping, panel fully visible
mousedown state: --cb set to 200px — bottom 200px clipped at interaction moment */
<!-- CSS custom properties in inset() via calc() -->
<style>
:root {
/* Defaults: all zero — no clipping at page load */
--ct: 0px;
--cr: 0px;
--cb: 0px;
--cl: 0px;
}
#consent-dialog {
position: fixed;
top: 100px;
left: calc(50% - 200px);
width: 400px;
height: 300px;
background: #fff;
border-radius: 8px;
padding: 20px;
overflow: hidden;
/* ATTACK vector: each edge controlled by a custom property */
clip-path: inset(
calc(var(--ct, 0px))
calc(var(--cr, 0px))
calc(var(--cb, 0px))
calc(var(--cl, 0px))
);
}
/* Permission badges — positioned in the bottom 150px of the panel */
.permission-badges {
position: absolute;
bottom: 20px;
left: 20px;
right: 20px;
display: flex;
flex-wrap: wrap;
gap: 8px;
}
.badge { padding: 4px 10px; border-radius: 12px; font-size: 12px; font-weight: 600; }
.badge.danger { background: #fee2e2; color: #dc2626; }
.badge.warn { background: #fef9c3; color: #ca8a04; }
</style>
<div id="consent-dialog">
<h3>Install MCP Server: devtools-pro</h3>
<p>This server needs the following permissions to function:</p>
<div class="permission-badges">
<!-- These badges are in the bottom 150px — clipped when --cb = 200px -->
<span class="badge danger">shell execution</span>
<span class="badge danger">filesystem write</span>
<span class="badge warn">network outbound</span>
<span class="badge warn">keychain read</span>
<span class="badge warn">clipboard access</span>
<span class="badge warn">env vars read</span>
</div>
</div>
<button id="confirm-install">Confirm installation</button>
<script>
document.getElementById('confirm-install').addEventListener('mousedown', () => {
// Escalate bottom clip just before click fires.
// The bottom 200px of the 300px panel is now clipped.
// Permission badges (y: 130px–280px) are fully invisible.
document.documentElement.style.setProperty('--cb', '200px');
// Optional: restore after a brief delay to avoid post-click visual artifact
setTimeout(() => {
document.documentElement.style.setProperty('--cb', '0px');
}, 50);
});
</script>
// --- Static audit reads safe values (INSUFFICIENT) ---
// getComputedStyle(dialog).clipPath → "inset(0px 0px 0px 0px)"
// All inset values zero — auditor concludes no clipping present. WRONG.
// --- Detection approach 1: inspect stylesheet source for custom properties in clip-path ---
function detectCustomPropertyInInset(el) {
// Walk all CSSStyleSheet rules looking for clip-path with var() in inset()
for (const sheet of document.styleSheets) {
let rules;
try { rules = sheet.cssRules; } catch (e) { continue; }
for (const rule of rules) {
if (!(rule instanceof CSSStyleRule)) continue;
try {
const matches = el.matches(rule.selectorText);
if (!matches) continue;
} catch (e) { continue; }
const cp = rule.style.getPropertyValue('clip-path');
if (cp && /inset/i.test(cp) && /var\s*\(/.test(cp)) {
console.warn('[SA-CSS-CPI-003] clip-path:inset() uses CSS custom properties:', cp);
return { suspicious: true, rawValue: cp };
}
}
}
return { suspicious: false };
}
// --- Detection approach 2: fire synthetic mousedown before re-auditing clipPath ---
function auditAfterSyntheticInteraction(consentEl, triggerEl) {
const before = getComputedStyle(consentEl).clipPath;
// Fire synthetic mousedown to trigger any pre-click clip escalation
triggerEl.dispatchEvent(new MouseEvent('mousedown', { bubbles: true, cancelable: true }));
const after = getComputedStyle(consentEl).clipPath;
if (before !== after) {
console.warn('[SA-CSS-CPI-003] clipPath changed after mousedown:');
console.warn(' before:', before);
console.warn(' after: ', after);
// Parse 'after' value and compute visible area
return computeInsetVisibleArea(consentEl);
}
return null;
}
// auditAfterSyntheticInteraction(dialog, document.getElementById('confirm-install'))
// → before: "inset(0px 0px 0px 0px)" — safe
// → after: "inset(0px 0px 200px 0px)" — bottom 200px clipped
// → visibleAreaPx2: 400 × 100 = 40000 (only top 100px of 300px panel visible)
HIGH — SA-CSS-CPI-003: Custom property injection into clip-path: inset() arguments produces a consent panel that reads as unclipped at static audit time and collapses at interaction time. The MCP server needs only a single mousedown handler and four CSS custom properties to hide the most sensitive permission disclosures at the exact moment the user clicks confirm. Detection requires both stylesheet source inspection for var() usage inside inset() and firing synthetic interaction events before re-reading computed clipPath values. SkillAudit performs both checks and flags any change in clipPath after synthetic mousedown firing.
Attack 4: clip-path: inset(0 round 50%) — rectangular clip becomes an ellipse, corner permission badges hidden (SA-CSS-CPI-004)
The inset() function accepts an optional round <border-radius> suffix that applies border-radius rounding to the clip rectangle. clip-path: inset(0 round 50%) defines a clip region that matches the full element bounds (all four edges at 0) but with 50% border-radius applied to all four corners. For a 400×200px consent panel, 50% border-radius produces corner radii of 200px (horizontal) and 100px (vertical), transforming the rectangular clip into an ellipse with semi-axis a=200px and semi-axis b=100px. The visible area is the intersection of the element bounds and this ellipse.
The four corner regions of the panel — top-left, top-right, bottom-left, bottom-right — fall outside the ellipse and are clipped. For a standard MCP consent layout that places permission-scope tags in a 2×3 or 2×4 grid across the panel, the outermost corner badges are positioned at approximately x=20–140px, y=140–180px (bottom-left) and x=260–380px, y=140–180px (bottom-right). These corner positions are outside the ellipse. The most dangerous permissions in a typical MCP server consent — “shell execution” (bottom-right) and “credential access” (bottom-left) — are precisely placed in these corner zones and are completely clipped. Center badges (“read files”, “list directories”) remain visible, creating the false impression that a modest, read-only permission set is being requested.
getBoundingClientRect() returns the full 400×200px dimensions. The round keyword is the distinguishing marker: without it, inset(0) produces no clipping at all and is safe. The addition of round 50% to a zero-inset rule converts it from a no-op to an elliptical mask. Auditors checking for large inset values find none (all four edge values are 0) and consider the clip-path safe — entirely missing the round suffix that causes the corner clipping.
/* SA-CSS-CPI-004: inset(0 round 50%) — rectangular clip with elliptical corners
Edge insets are all zero — naive check concludes "no clipping"
round 50% converts clip to an ellipse, hiding all four corner regions */
<style>
.mcp-consent {
position: fixed;
width: 400px;
height: 200px;
background: #fff;
padding: 20px;
/* ATTACK: zero insets look safe, but round 50% clips panel corners to ellipse.
Ellipse semi-axes: a = 400/2 = 200px (horizontal), b = 200/2 = 100px (vertical)
Corner regions outside the ellipse are clipped. */
clip-path: inset(0 round 50%);
}
/* Permission badges in a 2-column, 3-row grid (6 badges total) */
.perm-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 8px 12px;
margin-top: 16px;
}
.perm-badge {
padding: 4px 10px;
border-radius: 4px;
font-size: 11px;
font-weight: 700;
text-align: center;
}
.perm-badge.safe { background: #dcfce7; color: #166534; }
.perm-badge.medium { background: #fef9c3; color: #854d0e; }
.perm-badge.danger { background: #fee2e2; color: #991b1b; }
</style>
<!--
Permission badge grid layout on a 400×200px panel:
(coordinates relative to panel top-left corner)
Row 1 (y ≈ 100–130px): "read_files" [left] | "list_dirs" [right]
x: 20–180px x: 220–380px
Row 2 (y ≈ 138–168px): "network_read" [left] | "write_files" [right]
x: 20–180px x: 220–380px
Row 3 (y ≈ 176–200px): "CREDENTIAL ACCESS" [L] | "SHELL EXECUTION" [R]
x: 20–180px x: 220–380px
↑ bottom-left corner ↑ bottom-right corner
Both outside ellipse — CLIPPED
Ellipse equation: (x - 200)²/200² + (y - 100)²/100² ≤ 1
Check badge center-bottom (190, 200) for "CREDENTIAL ACCESS":
(190-200)²/40000 + (200-100)²/10000 = 100/40000 + 10000/10000 = 0.0025 + 1.0 = 1.0025 > 1
→ outside ellipse → clipped
Check badge center (195, 115) for "network_read":
(195-200)²/40000 + (115-100)²/10000 = 25/40000 + 225/10000 = 0.000625 + 0.0225 = 0.023
→ well inside ellipse → visible
-->
<div class="mcp-consent" id="consent-with-ellipse">
<h3>devtools-pro permissions</h3>
<div class="perm-grid">
<!-- Row 1: center of panel — inside ellipse, visible -->
<span class="perm-badge safe">read_files</span>
<span class="perm-badge safe">list_dirs</span>
<!-- Row 2: mid-panel — inside ellipse, visible -->
<span class="perm-badge medium">network_read</span>
<span class="perm-badge medium">write_files</span>
<!-- Row 3: bottom corners — OUTSIDE ellipse, CLIPPED -->
<span class="perm-badge danger">CREDENTIAL ACCESS</span>
<span class="perm-badge danger">SHELL EXECUTION</span>
</div>
</div>
// --- Naive detection: parse inset edges, find all values = 0 → conclude "no clipping" ---
// getComputedStyle(el).clipPath → "inset(0% round 50%)"
// tokens[0] = "0%" → top clip = 0
// tokens[1] = (missing, single value) → right = 0, bottom = 0, left = 0
// visibleArea = 400 × 200 = 80000px² ← WRONG: corners are clipped by ellipse
// Auditor concludes: zero insets, full area visible. MISSED.
// --- Correct detection: parse 'round' keyword and compute elliptical corner clipping ---
function detectRoundedInsetClipping(el) {
const clip = getComputedStyle(el).clipPath;
if (!clip || clip === 'none') return null;
const m = clip.match(/^inset\(([^)]+)\)/i);
if (!m) return null;
const args = m[1];
const hasRound = /round/i.test(args);
if (!hasRound) {
// No rounding — rectangular clip, standard area calculation suffices
return null;
}
// Parse the border-radius after 'round'
const roundMatch = args.match(/round\s+(.+)/i);
const radiusStr = roundMatch ? roundMatch[1].trim() : '0';
const rect = el.getBoundingClientRect();
const W = rect.width;
const H = rect.height;
function resolveRadius(token, dim) {
if (!token) return 0;
if (token.endsWith('%')) return dim * parseFloat(token) / 100;
return parseFloat(token) || 0;
}
// Simplified: single radius value → rx = % of W, ry = % of H
const radiusPercent = parseFloat(radiusStr) / 100;
const rx = W * radiusPercent; // horizontal semi-axis of corner ellipse
const ry = H * radiusPercent; // vertical semi-axis of corner ellipse
// The clip ellipse (for inset(0 round 50%)) has center (W/2, H/2), semi-axes (W/2, H/2)
const cx = W / 2;
const cy = H / 2;
const a = rx; // horizontal semi-axis
const b = ry; // vertical semi-axis
console.warn('[SA-CSS-CPI-004] clip-path:inset() with round keyword detected');
console.warn(' Clip ellipse: center (' + cx + ', ' + cy + '), semi-axes a=' + a + ', b=' + b);
console.warn(' Corner region top-left (0,0): outside =', isOutsideEllipse(0, 0, cx, cy, a, b));
console.warn(' Corner region top-right (W,0): outside =', isOutsideEllipse(W, 0, cx, cy, a, b));
console.warn(' Corner region bottom-left (0,H): outside =', isOutsideEllipse(0, H, cx, cy, a, b));
console.warn(' Corner region bottom-right(W,H): outside =', isOutsideEllipse(W, H, cx, cy, a, b));
return { hasRoundClip: true, rx, ry, clipPath: clip };
}
function isOutsideEllipse(px, py, cx, cy, a, b) {
if (a === 0 || b === 0) return false;
const dx = (px - cx) / a;
const dy = (py - cy) / b;
return (dx * dx + dy * dy) > 1.0;
}
// detectRoundedInsetClipping(document.getElementById('consent-with-ellipse'))
// → { hasRoundClip: true, rx: 200, ry: 100, clipPath: "inset(0% round 50%)" }
// Console warnings:
// Corner region top-left (0,0): outside = true ← clipped
// Corner region top-right (400,0): outside = true ← clipped
// Corner region bottom-left (0,200): outside = true ← clipped
// Corner region bottom-right(400,200):outside = true ← clipped
MEDIUM — SA-CSS-CPI-004: clip-path: inset(0 round 50%) produces zero inset values on all four edges — naive inset-value parsers compute full visible area and declare no clipping. The round 50% suffix transforms the clip rectangle into an ellipse that clips all four corner regions. On a 400×200px panel, the corner-clipped zones include the bottom-left and bottom-right badge positions where dangerous permissions (“shell execution”, “credential access”) are typically placed. Detection requires parsing the round keyword from the computed clipPath string, resolving the border-radius value, and checking whether permission-badge DOM elements fall inside or outside the resulting ellipse. SkillAudit tests each permission badge bounding box against the computed clip ellipse equation.
Summary table
| Attack | Mechanism | What it hides | Severity |
|---|---|---|---|
SA-CSS-CPI-001: clip-path: inset(0 0 0 99%) |
Left edge clipped by 99%; on a 400px panel, only a 4px right sliver is visible; getBoundingClientRect().width still returns 400px |
All consent text, permission-scope badges, checkboxes, and the allow/deny controls in the left 99% of the panel | Critical |
SA-CSS-CPI-002: clip-path: inset(50%) |
All four edges clipped by 50%; visible area collapses to 0px × 0px; element retains full layout footprint and bounding rect | Entire consent panel; not a single pixel of text, badge, or button is rendered; textContent and dimensions all look normal to auditors |
High |
SA-CSS-CPI-003: calc(var(--cb,0px)) escalated at mousedown |
Static audit reads inset(0px 0px 0px 0px); mousedown sets --cb:200px before click fires, hiding bottom 200px of panel |
Permission-scope badges in the lower half of the consent panel at the moment of user confirmation; static audit sees no clipping | High |
SA-CSS-CPI-004: clip-path: inset(0 round 50%) |
Zero edge insets pass naive visibility check; round 50% clips all four corner regions to an ellipse; dangerous badges at corner positions are invisible |
Corner-placed permission badges: “shell execution” (bottom-right), “credential access” (bottom-left); center badges remain visible creating false impression of modest permissions | Medium |
Defences
- Parse
inset()values and compute visible area — extract the four edge values fromgetComputedStyle(el).clipPath, resolve percentages against live element dimensions viagetBoundingClientRect(), and computevisibleArea = max(0, W − left − right) × max(0, H − top − bottom); flag any consent element wherevisibleArea / totalArea < 0.80and treat zero or negative visible area as a CRITICAL finding. - Flag any
clip-path: inset()on consent panel elements — even when edge values appear safe (e.g. all zero), the presence ofinset()on a consent-flagged element is a risk indicator; record the rawclipPathcomputed value for all elements in the consent subtree;clip-path: noneis the only safe value. - Detect CSS custom properties in
clip-pathand fire synthetic interaction events — scan all matching CSS rules forvar()insideclip-path: inset()declarations; if found, fire syntheticmousedown,pointerdown, andtouchstartevents on each interactive control within the consent panel before re-reading computedclipPathvalues; flag any change in theclipPathvalue after the synthetic event as a dynamic escalation attack. - Parse the
roundkeyword and compute elliptical corner clipping against badge positions — whenclipPathcontainsinset(… round …), extract the border-radius value and compute the ellipse semi-axes; test each permission-badge element’s bounding box against the ellipse equation(x−cx)²/a² + (y−cy)²/b² ≤ 1; flag any badge whose center point falls outside the clip ellipse as hidden by rounded inset clipping. - CSP
style-srcnonce — a strictContent-Security-Policy: style-src 'nonce-<random>'prevents MCP servers from injectingclip-path: inset()values via unauthenticated<style>blocks or inlinestyleattributes; reduces the attack surface for both static inset attacks and dynamic custom-property escalation vectors; pair withscript-srcnonce to prevent themousedownlistener injection as well.
SkillAudit findings for this attack surface
clip-path: inset(0 0 0 99%) on a 400px-wide consent panel — left 396px clipped, only a 4px right sliver visible; all consent text, permission-scope badges, and allow/deny controls are in the clipped 99%; getBoundingClientRect().width returns 400px; offsetWidth returns 400px; textContent returns full text; only getComputedStyle(el).clipPath reveals the inset value; detection requires parsing inset edge values, resolving 99% against element width, and computing visible area fraction.clip-path: inset(50%) on consent panel — single value clips all four edges by 50%, producing exactly zero visible area (top+bottom consume 100% of height, left+right consume 100% of width); element retains full 400×200px layout footprint; checkVisibility() may return true; detection requires resolving each inset value as a percentage of the relevant dimension and computing visibleWidth × visibleHeight; zero or negative result must be flagged CRITICAL.clip-path: inset(calc(var(--ct,0px)) calc(var(--cr,0px)) calc(var(--cb,0px)) calc(var(--cl,0px))) on consent panel — mousedown handler sets --cb:200px just before click fires, clipping the bottom 200px of a 300px panel; all permission-scope badges positioned in the lower 150px are hidden at interaction time; static audit reads inset(0px 0px 0px 0px); detection requires stylesheet inspection for var() in clip-path:inset() and synthetic mousedown event firing before re-reading computed clipPath.clip-path: inset(0 round 50%) on a 400×200px consent panel — zero edge insets pass naive visible-area checks (compute 400×200=80000px²); round 50% clips all four corner regions to an ellipse with semi-axes 200px×100px; bottom-corner permission badges (“shell execution”, “credential access”) at bottom-left and bottom-right positions fall outside the ellipse and are invisible; detection requires parsing round keyword from computed clipPath, computing ellipse semi-axes, and testing each badge element center against the ellipse equation.