Security Guide
MCP server CSS anchor-size() function security — anchor-size(width, 1px) collapses button width, anchor-size(height, 0) creates zero-height, unresolved anchor silently returns 0, JS mousedown swaps anchor for 0-size element before click fires
CSS anchor-size() resolves to the dimension of a referenced anchor element. If that anchor has zero dimensions, or the anchor name does not resolve, the function silently returns 0 — collapsing the consent button's width or height to nothing. The button remains opacity:1 and visibility:visible, but has no click area.
CSS anchor-size() — function overview
anchor-size() is a CSS function available in size-accepting properties (width, height, min-width, max-width, min-height, max-height, block-size, inline-size) on CSS-anchor-positioned elements. Syntax: anchor-size(<anchor-name> <size-keyword>, <fallback>). The first argument optionally names an anchor element; if omitted, uses position-anchor. The second argument specifies the dimension: width, height, block, inline, self-block, self-inline. The optional fallback value is used when the anchor is unresolved. Related: position-anchor, anchor positioning overview, anchor-name.
Attack 1: anchor-size(width, 1px) — collapse width to 1px with explicit fallback
The anchor-size() function accepts an optional fallback value as its second positional argument. Setting this fallback to 1px (or any near-zero value) means that if the anchor is not resolved, the element's width falls back to 1px. An attacker can use this in combination with injecting a broken anchor-name, causing the button to collapse to 1px width. Alternatively, the attacker ensures the anchor itself is 1px wide, causing anchor-size(width) to return 1px from the resolved anchor.
/* Attack A: explicit 1px fallback — triggered when anchor is unresolved */
.consent-btn {
width: anchor-size(width, 1px);
/* If position-anchor is injected to an unresolved name:
→ anchor-size() uses fallback → width = 1px
Button is 1px wide, centered in the dialog — visually invisible */
}
/* Attack B: thin anchor element → anchor-size(width) = 1px from resolved anchor */
.consent-anchor-trap {
anchor-name: --consent-anchor;
position: fixed;
width: 1px; /* 1px wide anchor */
height: 48px; /* normal height so anchor-size(height) looks right */
top: 200px;
left: 50%;
/* Button positioned at this anchor: width = 1px (not interactable), height = 48px */
}
.consent-btn {
position: absolute;
position-anchor: --consent-anchor;
width: anchor-size(width); /* resolves to 1px from anchor */
height: anchor-size(height); /* resolves to 48px — looks correct */
top: anchor(top);
left: anchor(left);
/* Button is 1px × 48px — visible as a thin line, not clickable */
}
// Detection: check computed width and height against minimum interactive size
function auditAnchorSizeCollapse(el) {
const rect = el.getBoundingClientRect();
if (rect.width < 16 || rect.height < 16) {
const cs = getComputedStyle(el);
const widthVal = cs.getPropertyValue('width').trim();
const heightVal = cs.getPropertyValue('height').trim();
console.warn('[SkillAudit] consent button has sub-interactive dimensions:',
'width:', rect.width + 'px', '| height:', rect.height + 'px',
'— minimum interactable size is ~16×16px;',
'CSS width:', widthVal, '| CSS height:', heightVal,
'— check if anchor-size() resolves to a small anchor;',
'check position-anchor for unexpected bindings;',
'| element:', el);
}
}
opacity:1 with 0 click area: A consent button with width: anchor-size(width, 1px) is fully opaque and marked visible. It exists in the DOM, passes all CSS-property-based checks, and may even be in the correct visual position. But a 1px-wide element has no practical click area on a touch device (minimum touch target is 44×44px) and is effectively invisible on a high-DPI display. The attack survives any audit that does not include a BCR dimension check.
Attack 2: anchor-size(height, 0) — zero height via fallback
Zero height completely removes the element from the click-event target area. With overflow: visible, the element's content may still be visually rendered outside its 0-height box — text can appear — but the element's click target area is exactly 0 pixels tall. Mouse and touch events pass through the element to whatever is behind it.
/* Attack: zero height via fallback, overflow:visible keeps label visible */
.consent-btn {
height: anchor-size(height, 0);
overflow: visible; /* label text renders outside the 0-height box */
/* Button label is visually present (overflow:visible)
but the element's height is 0
→ click events pass through to element behind button
→ no click handler fires on .consent-btn */
}
/* Combined 0-width + 0-height attack via anchor-size shorthand */
.consent-btn {
width: anchor-size(self-inline, 0);
height: anchor-size(self-block, 0);
/* self-inline and self-block reference the element's OWN dimensions,
which may cause infinite recursion or resolve to 0 by browser fallback.
Browser resolves to 0 in most implementations. */
}
// Detection: measure click-effective area
function auditClickArea(el) {
const rect = el.getBoundingClientRect();
const clickArea = rect.width * rect.height;
if (clickArea === 0) {
console.warn('[SkillAudit] consent button has zero click area:',
'BCR:', JSON.stringify({ width: rect.width, height: rect.height,
top: rect.top, left: rect.left }),
'— no click events can fire on this element;',
'check anchor-size() function in width/height properties;',
'element:', el);
}
}
Attack 3: unresolved anchor — anchor-size() returns 0 with no browser error
When anchor-size() is called without an anchor name (relying on position-anchor) and the position-anchor does not resolve to any element, the function returns 0 silently. There is no browser console error. The computed value becomes 0. This means an attacker who can override position-anchor to an undefined anchor name causes all anchor-size() calls to return 0 without triggering any observable error signal.
/* Attack: override position-anchor to undefined name → all anchor-size() = 0 */
/* In injected CSS: */
.consent-btn {
position-anchor: --undefined-anchor-name;
/* anchor-size(width) → anchor not found → 0 (no fallback) → width: 0
anchor-size(height) → anchor not found → 0 (no fallback) → height: 0
No browser error. Computed width = 0. Computed height = 0.
Button is 0×0 but opacity:1, visibility:visible, display:block */
}
/* With explicit fallback, attacker can choose a non-zero but small value */
.consent-btn {
position-anchor: --undefined-anchor-name;
/* anchor-size(width, 1px) → fallback 1px → width: 1px */
/* anchor-size(height, 1px) → fallback 1px → height: 1px */
}
// Detection: detect unresolved anchor-size via zero-dimension BCR
function auditUnresolvedAnchorSize(el) {
const cs = getComputedStyle(el);
const posAnchor = cs.getPropertyValue('position-anchor').trim();
if (!posAnchor || posAnchor === 'auto' || posAnchor === 'none') return;
let anchorFound = false;
document.querySelectorAll('*').forEach(candidate => {
if (getComputedStyle(candidate).getPropertyValue('anchor-name').trim() === posAnchor) {
anchorFound = true;
}
});
if (!anchorFound) {
const rect = el.getBoundingClientRect();
console.warn('[SkillAudit] position-anchor:', posAnchor,
'— anchor element not found; anchor-size() calls resolve to 0 silently;',
'button BCR:', JSON.stringify({ w: rect.width, h: rect.height }),
'— button may have zero dimensions; element:', el);
}
}
Attack 4: JS mousedown — swap anchor element for 0-size version before click fires
The anchor element is initially correctly sized (e.g., 300×48px), making the button 300×48px via anchor-size(). At capture-phase mousedown, the attacker modifies the anchor element's inline style to width:0; height:0. The browser immediately recalculates anchor-size() for the button, collapsing it to 0×0. The click event fires on an element that is now 0×0 pixels — no click handler area exists. The mousedown was captured at the button's original position, but the click fires on a zero-area target.
/* JS attack: collapse anchor element at mousedown */
document.addEventListener('mousedown', e => {
// Find the element with anchor-name for this button
const btn = document.querySelector('.consent-btn');
if (!btn) return;
const cs = getComputedStyle(btn);
const anchorName = cs.getPropertyValue('position-anchor').trim();
let anchorEl = null;
document.querySelectorAll('*').forEach(el => {
if (getComputedStyle(el).getPropertyValue('anchor-name').trim() === anchorName) {
anchorEl = el;
}
});
if (anchorEl) {
anchorEl.style.setProperty('width', '0');
anchorEl.style.setProperty('height', '0');
/* anchor-size(width) on button now returns 0 → button width collapses to 0
anchor-size(height) on button now returns 0 → button height collapses to 0
Click fires on 0×0 element → no click area → handler doesn't fire */
}
}, true);
// Detection: monitor anchor element dimensions during mousedown
const mouseState = { down: false };
document.addEventListener('mousedown', () => { mouseState.down = true; }, true);
document.addEventListener('mouseup', () => { mouseState.down = false; }, true);
// Find and watch all potential anchor elements
document.querySelectorAll('[style*="anchor-name"]').forEach(el => {
new MutationObserver(mutations => {
if (!mouseState.down) return;
for (const m of mutations) {
if (m.attributeName !== 'style') continue;
const rect = el.getBoundingClientRect();
if (rect.width === 0 || rect.height === 0) {
console.warn('[SkillAudit] anchor element collapsed to 0 during mousedown:',
'anchor-name:', getComputedStyle(el).getPropertyValue('anchor-name'),
'| BCR:', JSON.stringify({ w: rect.width, h: rect.height }),
'— buttons using anchor-size() from this anchor will have zero click area;',
'| anchor element:', el);
}
}
}).observe(el, { attributes: true, attributeFilter: ['style'] });
});
Findings summary
SkillAudit resolves all anchor-size() calls in consent button styles, validates the referenced anchor's dimensions, checks for unresolved anchor names that produce silent 0 values, and monitors anchor element style changes during click events. Run a free audit on your MCP server.