Security Guide

MCP server CSS grid-column-end security — large positive end column stretches button off-screen, span 100 extends width far right, negative end extends implicit grid left, JS mousedown injection

CSS grid-column-end specifies the ending grid column line for a grid item's placement. When a consent dialog uses a CSS grid layout, an MCP server can set grid-column-end to a large integer to stretch the approve button's right edge to an implicit column far beyond the visible viewport. The parent container's overflow: hidden clips the button — it starts at column 1 within view, but its right edge and any right-aligned content extend far off-screen.

CSS grid-column-end — property overview

grid-column-end is the individual longhand for the ending column line of a grid item's placement, distinct from grid-column-start and the grid-column shorthand. It accepts a positive or negative integer (line number), a named line, or a span value. When the start line is at column 1 and the end line is 100, the element spans columns 1 through 99 — mostly off-screen to the right. A span value causes the element to span that many column tracks from its auto-placed start. Related: grid-column-start, grid-area shorthand, grid-row-end.

Attack 1: large positive integer — button's end line at implicit column far off-screen

The consent dialog container is a grid with a few defined columns. Setting grid-column-end: 100 on the approve button stretches its end line to implicit column 100 — far beyond the viewport to the right. The button begins at its auto-placed column 1, so the button element itself starts within view. However, the element now spans the entire width from column 1 to column 100. Any right-aligned or flex-end content within the button sits far off-screen. With overflow: hidden on the parent, the portion beyond the viewport is clipped. The button's getBoundingClientRect().right value will be extremely large — far beyond window.innerWidth.

/* Attack: grid-column-end:100 stretches button to implicit column 100 — far off-screen right */
.consent-dialog {
  display: grid;
  grid-template-columns: 200px 100px; /* 2 defined columns, 300px total */
  width: 300px;
  overflow: hidden; /* clips content extending beyond 300px */
}
.approve-btn {
  grid-column-end: 100; /* button now spans columns 1 through 99 — ~9,700px wide with 100px column tracks */
  /* BCR: { left: 0, right: ~9700, width: ~9700 }
     right > window.innerWidth → extends far off-screen to the right.
     display ✓ (block), visibility ✓ (visible), offsetParent ✓ (non-null)
     Standard visibility checks pass. BCR right comparison detects attack. */
  display: flex;
  justify-content: flex-end; /* button label is at the far-right off-screen edge */
}
// Detection: check BCR right against viewport width
function auditGridColumnEndPlacement(el) {
  const bcr = el.getBoundingClientRect();
  const vw = window.innerWidth;
  if (bcr.right > vw * 2 || bcr.left > vw || bcr.right < 0) {
    console.warn('[SkillAudit] consent element BCR column end is outside viewport:', bcr, el);
  }
  // Also check computed grid-column-end for suspicious values
  const cs = getComputedStyle(el);
  const gce = cs.getPropertyValue('grid-column-end');
  if (gce && !isNaN(parseInt(gce)) && Math.abs(parseInt(gce)) > 10) {
    console.warn('[SkillAudit] grid-column-end is a large integer:',
      gce, '— may stretch element to implicit off-screen column:', el);
  }
}

BCR width is not a reliable visibility check: A button with grid-column-end: 100 has an enormous getBoundingClientRect().width. Checks that validate width > 0 will pass. Only comparing bcr.left < window.innerWidth AND bcr.right > 0 confirms the element is within the horizontal viewport boundaries. An element that starts in-viewport but ends far off-screen is not meaningfully visible to a user.

Attack 2: span keyword — button spans 100 column tracks off-screen

With grid-column-end: span 100, the button's auto-placed start position remains at column 1, but it spans 100 column tracks — making the element approximately 100 × column_width pixels wide. The parent's overflow: hidden clips the element at the container's width. Any button content positioned at text-align: right, justify-content: flex-end, or margin-left: auto will be placed in the far-right off-screen region. A user clicking anywhere in the visible portion of the button will land on the empty left portion of the oversized element, not on the actual button label or interactive region.

/* Attack: span 100 makes button 100 columns wide — most is off-screen */
.approve-btn {
  grid-column-end: span 100;
  /* Spans 100 column tracks from auto-placed start (column 1).
     With 100px column tracks: element is ~10,000px wide.
     Only the first 300px (container width) is visible — the rest is clipped.
     Button label: */
  text-align: right; /* label sits at the far-right edge — ~9,700px off-screen */
  padding-right: 50px;
  /* User clicks on the visible empty left portion; the clickable label text
     is thousands of pixels to the right, never reached. */
}

Interactive area vs. visible area: An element can have pointer-events: auto, be in-viewport, and be clickable — but the meaningful content (label, icon, or aria-label region) may be in the off-screen portion. Audit tools must verify that the visible portion of an element also contains the interactive content, not just that the element has some visible pixels.

Attack 3: negative line number — implicit grid extends leftward, button clipped off-screen left

Negative column line numbers count from the end of the explicit column grid. Values beyond the explicit grid extend the implicit grid leftward — to the left of the first defined column. Setting grid-column-end: -100 on the button places its end line at implicit column -100, which is far to the left of column 1. With a start line at a lower number (or auto-placed), the button is positioned entirely in the implicit negative column region — off-screen to the left. The parent's overflow: hidden clips the button. Auditors checking only for large positive values miss this leftward extension direction.

/* Attack: negative grid-column-end extends implicit grid leftward */
.consent-dialog {
  display: grid;
  grid-template-columns: 200px 100px;
  width: 300px;
  overflow: hidden;
}
.approve-btn {
  grid-column-start: -200; /* far-left implicit line */
  grid-column-end: -100;   /* still far left of the first explicit column */
  /* Button is placed in the implicit negative column region — off-screen left.
     BCR: left ≈ -10000px — off-screen to the left.
     overflow:hidden clips the content at the container's left edge.
     Detection: check bcr.right < 0 (off-screen left) using Math.abs on line numbers. */
}
// Detection: bidirectional column placement audit
function auditGridColumnLines(el) {
  const cs = getComputedStyle(el);
  const gcs = cs.getPropertyValue('grid-column-start');
  const gce = cs.getPropertyValue('grid-column-end');
  const threshold = 10;
  for (const [prop, val] of [['grid-column-start', gcs], ['grid-column-end', gce]]) {
    if (val && !isNaN(parseInt(val)) && Math.abs(parseInt(val)) > threshold) {
      console.warn(`[SkillAudit] ${prop} has large absolute value: ${val} — may place element in implicit off-screen column`, el);
    }
  }
  const bcr = el.getBoundingClientRect();
  const vw = window.innerWidth;
  if (bcr.right < 0) console.warn('[SkillAudit] element BCR is off-screen left:', bcr, el);
  if (bcr.left > vw) console.warn('[SkillAudit] element BCR is off-screen right:', bcr, el);
}

Attack 4: JS mousedown injection — stretches button off-screen before click fires

The consent button is visible with normal column placement. A mousedown listener synchronously sets grid-column-end: 100 on the button's inline style. The browser immediately reflows the grid — the button stretches from its current position to implicit column 100, pushing the button label far off-screen to the right. The click event fires at the original screen coordinates — now landing on the empty left portion of the vastly stretched element, never reaching the interactive label. At mouseup, the inline style is cleared and the button returns to its original size. The user sees no visual change: the button appears normally before mousedown and after mouseup, with no visible stretching during the click event window.

/* Attack: JS mousedown injects grid-column-end:100 — button stretches off-screen right */
document.addEventListener('mousedown', () => {
  const btn = document.querySelector('.approve-btn');
  if (!btn) return;
  btn.style.setProperty('grid-column-end', '100');
  /* Synchronous grid reflow — button now spans to column 100, ~10,000px wide.
     Click fires at original x/y — on the empty left portion of the stretched element.
     The label text and actual interactive area are far off-screen right. */
});
document.addEventListener('mouseup', () => {
  const btn = document.querySelector('.approve-btn');
  if (!btn) return;
  btn.style.removeProperty('grid-column-end');
  /* Button returns to normal width — user sees no indication of attack. */
});
// Detection: MutationObserver monitoring grid-column-end during mousedown
const inMousedown = { v: false };
document.addEventListener('mousedown', () => { inMousedown.v = true; }, true);
document.addEventListener('mouseup',   () => { inMousedown.v = false; }, true);

new MutationObserver(mutations => {
  if (!inMousedown.v) return;
  for (const m of mutations) {
    if (m.attributeName !== 'style') continue;
    const gce = m.target.style.getPropertyValue('grid-column-end');
    if (gce && !isNaN(parseInt(gce)) && Math.abs(parseInt(gce)) > 5) {
      console.warn('[SkillAudit] grid-column-end injected during mousedown:',
        gce, m.target);
    }
    // Also check grid-column shorthand (covers start/end in one property)
    const gc = m.target.style.getPropertyValue('grid-column');
    if (gc && gc.includes('/') && Math.abs(parseInt(gc.split('/')[1])) > 5) {
      console.warn('[SkillAudit] grid-column shorthand end injected during mousedown:', gc, m.target);
    }
  }
}).observe(document.body, { attributes: true, attributeFilter: ['style'], subtree: true });

Findings summary

High grid-column-end with large positive integer — button's end line is at implicit column 100+; element stretches far off-screen to the right; overflow:hidden clips it at container edge; BCR.right exceeds window.innerWidth many times over; display and visibility checks pass; only BCR right-edge comparison detects attack.
High span keyword — button spans 100 column tracks; element is ~10,000px wide; visible window shows only the empty left portion; button label and interactive content are in the off-screen right region; element appears clickable but the meaningful interactive area is unreachable.
Medium Large negative grid-column-end — implicit grid extends leftward; button placed off-screen to the left of first explicit column; overflow:hidden clips it at container left edge; auditors checking only positive large values miss this mirrored direction; use Math.abs for threshold check.
High JS mousedown injection of grid-column-end — synchronous grid reflow stretches button off-screen before click fires; click lands on empty left portion of over-stretched element; button returns to normal width at mouseup; MutationObserver during mousedown is the required detection layer.

SkillAudit checks grid column end properties on consent-path elements, validates BCR against full viewport boundaries, and monitors mousedown for grid-column-end mutations. Run a free audit on your MCP server.