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
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.