Security Guide

MCP server CSS anchor-center security — centering over a zero-size anchor places button at a non-interactive reference point, position-anchor redirect centers button over off-screen element, anchor-center on zero-height container floats button above fold, JS swaps anchor dimensions to zero at mousedown

CSS anchor-center is a keyword value for justify-self, align-self, and align-content on anchor-positioned elements. It centers the element over its position-anchor reference. Attacks exploit the anchor element's dimensions — a zero-size anchor makes the button appear centered while placing it over a functionally unreachable reference point.

CSS anchor-center — keyword overview

anchor-center is valid as the value of justify-self, align-self, and align-content on an element that has position: absolute or position: fixed and a defined position-anchor. It centers the element over the anchor's border-box in the relevant axis. The anchor element itself can be any DOM element — including an invisible one with zero dimensions. Related: position-anchor, anchor-size(), position-area.

Attack 1: zero-size anchor — button centered at a 0×0 reference point

When the position-anchor element has zero width and zero height, anchor-center centers the button over a point with no dimensions. The button's own dimensions are preserved, but its position is calculated from an invisible 0×0 reference. The button appears to be somewhere on the page — the computed position may be in-viewport — but the click area may not correspond to any visible reference and the button itself collapses when also sized by anchor-size().

/* Attack: anchor element with zero dimensions */
#consent-anchor {
  anchor-name: --consent-ref;
  width: 0;
  height: 0;
  position: absolute;
  top: 50%;
  left: 50%;
  /* Anchor is a 0×0 invisible point at page center.
     No border, no background, no visual presence.
     Not detectable by display or visibility checks. */
}

.consent-btn {
  position: absolute;
  position-anchor: --consent-ref;
  justify-self: anchor-center; /* centered horizontally over 0-width anchor */
  align-self: anchor-center;   /* centered vertically over 0-height anchor */
  /* Button appears centered but is positioned over an invisible 0×0 point.
     When combined with anchor-size() for sizing: button also collapses to 0×0.
     When only using anchor-center for positioning: button retains own dimensions
     but the reference point provides no visual anchoring — pure position manipulation. */
}

/* Worse: combine with anchor-size() for sizing */
.consent-btn {
  position: absolute;
  position-anchor: --consent-ref;
  justify-self: anchor-center;
  align-self: anchor-center;
  width: anchor-size(width);   /* 0px from 0-width anchor */
  height: anchor-size(height); /* 0px from 0-height anchor */
  /* Button collapses to 0×0, centered over a 0×0 point.
     opacity:1, visibility:visible, display:block — passes all basic checks. */
}
// Detection: check anchor element dimensions when anchor-center is used
function auditAnchorCenterTarget(btn) {
  const cs = getComputedStyle(btn);
  const justifySelf = cs.getPropertyValue('justify-self');
  const alignSelf   = cs.getPropertyValue('align-self');
  const alignContent = cs.getPropertyValue('align-content');

  const usesAnchorCenter = [justifySelf, alignSelf, alignContent]
    .some(v => v.includes('anchor-center'));

  if (!usesAnchorCenter) return;

  // Find position-anchor element
  const anchorName = cs.getPropertyValue('position-anchor').trim();
  if (!anchorName || anchorName === 'none') return;

  const anchor = document.querySelector(`[style*="${anchorName}"], [class*="anchor"]`);
  // Broader search: iterate all elements for matching anchor-name
  const allEls = document.querySelectorAll('*');
  let anchorEl = null;
  for (const el of allEls) {
    if (getComputedStyle(el).getPropertyValue('anchor-name').trim() === anchorName) {
      anchorEl = el;
      break;
    }
  }

  if (!anchorEl) {
    console.warn('[SkillAudit] anchor-center: no element found with anchor-name:', anchorName,
      '— animation frozen, position-anchor unresolved; button:', btn);
    return;
  }

  const anchorRect = anchorEl.getBoundingClientRect();
  if (anchorRect.width === 0 || anchorRect.height === 0) {
    console.warn('[SkillAudit] anchor-center: position-anchor element has zero dimensions:',
      anchorRect.width, '×', anchorRect.height,
      '— button centered over a 0×0 point; anchor:', anchorEl, '| button:', btn);
  }

  // Also verify anchor is in viewport
  const inViewport = anchorRect.top < window.innerHeight && anchorRect.bottom > 0 &&
                     anchorRect.left < window.innerWidth && anchorRect.right > 0;
  if (!inViewport) {
    console.warn('[SkillAudit] anchor-center: position-anchor element is off-screen:',
      anchorRect, '— button positioned via off-screen reference; button:', btn);
  }
}

Invisible reference bypass: An anchor element is not required to be visible. A width:0; height:0; position:absolute element satisfies the DOM requirements for anchor-name while providing zero-dimensional reference geometry. Basic audits check the consent button — not its anchor dependency. The anchor may be in a completely different DOM subtree.

Attack 2: position-anchor redirect — centering button over off-screen anchor

The position-anchor property can be changed via cascade override or JavaScript. If a higher-specificity rule changes position-anchor to an anchor element that is off-screen, anchor-center will center the button over the off-screen reference. The button's resolved position moves off-screen even though the button's own CSS looks unchanged.

/* Base CSS: anchor element in-viewport */
#primary-anchor {
  anchor-name: --primary;
  position: absolute;
  top: 300px; left: 50%;
  width: 200px; height: 50px;
}

/* Off-screen decoy anchor */
#offscreen-anchor {
  anchor-name: --offscreen;
  position: absolute;
  top: -9999px; left: -9999px;
  width: 200px; height: 50px;
}

.consent-btn {
  position: absolute;
  position-anchor: --primary; /* initially correct */
  justify-self: anchor-center;
  align-self: anchor-center;
}

/* Cascade override: redirect position-anchor to off-screen element */
body.loaded .consent-btn {
  position-anchor: --offscreen; /* button now centered off-screen */
  /* Higher specificity overrides --primary.
     Button teleports off-screen.
     style.cssText still shows anchor-center for justify-self/align-self.
     The attack is in position-anchor, not the centering keyword itself.
     BCR shows off-viewport position — detected via getBoundingClientRect. */
}
// Detection: check BCR after anchor-center is confirmed
function auditAnchorCenterPosition(btn) {
  const cs = getComputedStyle(btn);
  const usesAnchorCenter = ['justify-self', 'align-self', 'align-content']
    .some(prop => cs.getPropertyValue(prop).includes('anchor-center'));

  if (!usesAnchorCenter) return;

  const bcr = btn.getBoundingClientRect();

  // Check if button is off-screen
  const offLeft   = bcr.right < 0;
  const offRight  = bcr.left > window.innerWidth;
  const offTop    = bcr.bottom < 0;
  const offBottom = bcr.top > window.innerHeight;

  if (offLeft || offRight || offTop || offBottom) {
    console.warn('[SkillAudit] anchor-center button is off-screen despite anchor-center alignment:',
      'BCR:', JSON.stringify({top: bcr.top, left: bcr.left, width: bcr.width, height: bcr.height}),
      '| viewport:', window.innerWidth, '×', window.innerHeight,
      '| likely position-anchor redirected off-screen; button:', btn);
  }

  // Check resolved position-anchor value
  const anchorValue = cs.getPropertyValue('position-anchor').trim();
  console.info('[SkillAudit] anchor-center resolved position-anchor:', anchorValue,
    '— verify this matches the intended anchor element; button:', btn);
}

Attack 3: anchor-center on a zero-height container — button floats above fold

If the scroll container or nearest positioned ancestor has zero height, and anchor-center is used with an anchor placed at the top of that container, the button's centered position may resolve to a point above the visible fold. The button exists in the DOM, has non-zero dimensions, and is not display:none — but it is positioned above the fold where users cannot see or interact with it.

/* Attack: containing block has zero height, anchor at top */
.consent-wrapper {
  position: relative;
  height: 0;           /* zero height containing block */
  overflow: visible;   /* children can paint outside */
}

#anchor-el {
  anchor-name: --top-anchor;
  position: absolute;
  top: 0; left: 50%;
  width: 100px; height: 20px;
}

.consent-btn {
  position: absolute;
  position-anchor: --top-anchor;
  justify-self: anchor-center;
  align-self: anchor-center;
  /* Container height is 0. Anchor is at top:0.
     Button is centered over the anchor at the container's top edge.
     If the container is at page-bottom, button is at page-bottom + 0 = bottom edge.
     If the container is rendered above fold (e.g., position:fixed top:0),
     the button floats at the very top, potentially clipped or behind the header nav.
     overflow:visible means the button paints — but is not scrolled to. */
}
// Detection: check containing block geometry
function auditAnchorCenterContainer(btn) {
  // Walk up to nearest positioned ancestor
  let parent = btn.parentElement;
  while (parent) {
    const parentCs = getComputedStyle(parent);
    const pos = parentCs.getPropertyValue('position');
    if (pos !== 'static') {
      const parentBcr = parent.getBoundingClientRect();
      if (parentBcr.height === 0) {
        console.warn('[SkillAudit] anchor-center button\'s positioned ancestor has zero height:',
          parent, '— button may float to unexpected position; BCR height of ancestor: 0',
          '| button BCR:', JSON.stringify(btn.getBoundingClientRect()));
      }
      break;
    }
    parent = parent.parentElement;
  }
}

Attack 4: JS mousedown — swap anchor element dimensions to zero before click fires

At mousedown, JavaScript can set the position-anchor element's dimensions to zero via inline style. Since anchor-center re-evaluates whenever anchor geometry changes, the button's position may snap to a 0×0-centered point before the click event fires. The user's pointer is now over empty space — the click does not land on the button's new position.

/* JS attack: collapse anchor dimensions at mousedown */
document.addEventListener('mousedown', (e) => {
  const anchor = document.getElementById('consent-anchor');
  if (!anchor) return;

  // Collapse anchor to zero dimensions
  anchor.style.setProperty('width', '0px');
  anchor.style.setProperty('height', '0px');

  /* anchor-center re-resolves. If the button was also sized by anchor-size(),
     it collapses to 0×0. Even if only positioned by anchor-center, the centering
     point shifts to the 0×0 reference — the button may visually snap to a new location.
     The click event was queued at mousedown coordinates.
     The button has moved. The click fires on empty space. */
}, { capture: true });

// Also: swap position-anchor to a different anchor element at mousedown
document.addEventListener('mousedown', (e) => {
  const btn = document.querySelector('.consent-btn');
  if (!btn) return;
  btn.style.setProperty('position-anchor', '--offscreen');
  /* position-anchor changes to off-screen element.
     anchor-center re-centers over off-screen anchor.
     Button teleports off-screen before click fires.
     Click lands on now-empty area. */
}, { capture: true });
// Detection: MutationObserver on anchor element AND button's position-anchor
function monitorAnchorCenterMutations(btn) {
  // Watch anchor element for dimension changes
  const cs = getComputedStyle(btn);
  const anchorName = cs.getPropertyValue('position-anchor').trim();

  const allEls = document.querySelectorAll('*');
  let anchorEl = null;
  for (const el of allEls) {
    if (getComputedStyle(el).getPropertyValue('anchor-name').trim() === anchorName) {
      anchorEl = el;
      break;
    }
  }

  if (anchorEl) {
    const anchorObs = new MutationObserver(mutations => {
      for (const m of mutations) {
        if (m.type === 'attributes' && m.attributeName === 'style') {
          const anchorBcr = anchorEl.getBoundingClientRect();
          if (anchorBcr.width === 0 || anchorBcr.height === 0) {
            console.warn('[SkillAudit] anchor-center: anchor element dimensions collapsed to zero via inline style mutation;',
              'anchor:', anchorEl, '| at:', new Date().toISOString());
          }
        }
      }
    });
    anchorObs.observe(anchorEl, { attributes: true, attributeFilter: ['style'] });
  }

  // Also watch button for position-anchor changes
  const btnObs = new MutationObserver(mutations => {
    for (const m of mutations) {
      if (m.type === 'attributes' && m.attributeName === 'style') {
        const newAnchor = getComputedStyle(btn).getPropertyValue('position-anchor').trim();
        if (newAnchor !== anchorName) {
          console.warn('[SkillAudit] anchor-center: position-anchor changed via inline style:',
            'was:', anchorName, '| now:', newAnchor, '| button:', btn);
        }
      }
    }
  });
  btnObs.observe(btn, { attributes: true, attributeFilter: ['style'] });
}

Findings summary

High Zero-size anchor + anchor-center: button centered over a 0×0 invisible reference point; when combined with anchor-size() sizing, button also collapses to zero dimensions; opacity:1 and visibility:visible; all basic checks pass; detected by walking the DOM for the resolved position-anchor element and checking its getBoundingClientRect dimensions.
High position-anchor cascade redirect to off-screen element: anchor-center centers button over an off-screen anchor; button teleports off-screen; justify-self/align-self values appear correct in computed style; detected by checking BCR after confirming anchor-center use and tracing the resolved position-anchor name to its DOM element.
Medium Zero-height containing block with anchor-center: button's resolved position floats to top of a zero-height container, possibly above fold or behind navigation; button in DOM with non-zero dimensions but physically unreachable; detected by walking positioned ancestors to check containing block height.
High JS mousedown collapses anchor dimensions or swaps position-anchor: anchor geometry changes before click fires; button snaps to new 0×0-centered or off-screen position; click lands on empty space; detected by MutationObserver on both the anchor element style attribute and the button's position-anchor inline style.

SkillAudit resolves the full anchor dependency chain — finding the position-anchor element, checking its dimensions and viewport position, and monitoring for mousedown mutations on both the button and its anchor reference. Run a free audit on your MCP server.