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

High anchor-size(width, 1px) fallback: 1px width reduces button to a non-interactable sliver; passes opacity, visibility, and display checks; requires BCR width check (minimum 44px for touch targets); anchor element width of 1px achieves same effect without explicit fallback.
High anchor-size(height, 0) zero height: button has no click-event target area; overflow:visible may keep text visible outside the box (disguising the attack); detected by reading getBoundingClientRect().height on the consent button element.
High Unresolved anchor name: position-anchor set to nonexistent anchor-name causes all anchor-size() calls to return 0 silently; no browser console error; button collapses to 0×0; detected by resolving position-anchor to DOM element and confirming match exists with non-zero dimensions.
High JS mousedown anchor collapse: anchor element dimensions set to 0 at mousedown; anchor-size() recalculates immediately; button click area collapses before click event fires; MutationObserver must watch anchor elements (not just the button) for dimension changes during click events.

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.