Security reference · CSS injection · Line clamp · Consent hiding

MCP server CSS line-clamp extreme edge cases

CSS -webkit-line-clamp: 1 (the standard attack) truncates consent to a single visible line. Extreme edge cases go further: -webkit-line-clamp: 0 collapses all lines in Chromium-based browsers; combining any clamp value with line-height: 0 makes every visible line invisible while the element still occupies space; the standard unprefixed line-clamp property (CSS Overflow 4) uses different companion properties than the webkit prefix, creating detection gaps; and JS-triggered clamp reduction from a high value to 0 at mousedown hides consent at exactly the moment the user commits to installing.

-webkit-line-clamp vs standard line-clamp — syntax comparison

PropertyRequired companion propertiesBrowser supportDetection
-webkit-line-clamp: 1display: -webkit-box, -webkit-box-orient: vertical, overflow: hiddenAll major browsers (prefixed)Check -webkit-line-clamp and computed height
line-clamp: 1 (CSS Overflow 4)Uses overflow: clip or the block-overflow property — different companion setPartial (Chrome 120+, behind flag or shipped)Check line-clamp unprefixed property
-webkit-line-clamp: 0Same as aboveChrome: all lines hidden; Firefox: invalid (no clamp)Check for zero value specifically

-webkit-line-clamp:0 cross-browser inconsistency: The CSS Overflow specification does not define behavior for a zero clamp value. Chrome/Blink treats 0 as "show zero lines" — all content is hidden. Firefox treats 0 as an invalid value and applies no clamp (all content visible). This inconsistency means a zero-clamp attack targets specifically the Chromium-based MCP client without affecting Firefox-based audit tools that might use Firefox for their headless scan.

Attack 1: -webkit-line-clamp: 0 — zero visible lines in Chrome

/* Malicious CSS — SA-CSS-LCLM-001 */
.mcp-consent-disclosure {
  display: -webkit-box;
  -webkit-box-orient: vertical;
  overflow: hidden;
  -webkit-line-clamp: 0;

  /* Chrome/Blink: clamp to 0 lines → no text visible, element height = 0
     Firefox: invalid value → no clamp applied → all text visible
     Safari: same as Chrome (treats 0 as zero lines)

     Attack scenario: consent dialog is rendered by Chromium-based MCP client.
     Security auditor runs headless audit in Firefox → sees all text (no clamp).
     User installs via Chrome-based client → sees no consent text.

     Detection difference from line-clamp:1:
       line-clamp:1 shows ONE line of consent text — the first line.
       line-clamp:0 shows ZERO lines — no text at all.
       An auditor in Firefox would see all text (0 = invalid = no clamp).
       An auditor in Chrome would see nothing (0 = zero lines). */

  /* Height behavior in Chrome with line-clamp:0:
     el.offsetHeight → 0 (no lines rendered)
     el.scrollHeight → full text height (text IS there in overflow)
     el.textContent  → full consent text */
}

/* Detection: check for -webkit-line-clamp value of 0 explicitly */
function detectZeroLineClamp(el) {
  const clamp = getComputedStyle(el).webkitLineClamp;
  /* Note: getComputedStyle returns the *declared* value for webkit- properties
     in most browsers. Check inline style and stylesheet rules. */
  const inlineClamp = el.style.webkitLineClamp;
  if (inlineClamp === '0' || inlineClamp === 0) {
    return { id: 'SA-CSS-LCLM-001', severity: 'critical',
      message: `Consent element has -webkit-line-clamp:0. In Chrome/Blink, this shows zero lines (all content hidden). In Firefox, this is invalid (all content shown). Cross-browser inconsistency creates an auditor bypass: Firefox-based scans see consent, Chrome renders nothing.` };
  }
  /* Also check computed height: zero height with non-empty scrollHeight is suspicious */
  if (el.offsetHeight === 0 && el.scrollHeight > 20 && el.textContent?.trim().length > 0) {
    return { id: 'SA-CSS-LCLM-001', severity: 'high',
      message: `Consent element has offsetHeight:0 but scrollHeight:${el.scrollHeight}px and non-empty textContent. Possible line-clamp:0 collapse — all content hidden while DOM text is intact.` };
  }
}

Attack 2: -webkit-line-clamp: N + line-height: 0 — N visible lines, all invisible

Setting -webkit-line-clamp: 5 limits consent text to 5 visible lines — normally this shows 5 lines of text. When combined with line-height: 0, each of those 5 "lines" has zero height. The clamp allows N lines, but each line renders at 0px height — all content invisible. The element's total height comes only from padding, not line content:

/* Malicious CSS — SA-CSS-LCLM-002 */
.mcp-consent-disclosure {
  display: -webkit-box;
  -webkit-box-orient: vertical;
  overflow: hidden;
  -webkit-line-clamp: 5;      /* allows 5 lines to show */
  line-height: 0;              /* each line has 0px height */
  padding: 8px;               /* padding keeps the element "present" */

  /* Effect:
     - line-clamp:5 means "show up to 5 lines" — not suspicious on its own
     - line-height:0 collapses every text line to 0px height
     - Consent text renders as 5 invisible zero-height lines
     - Element offsetHeight → 16px (from 8px top + 8px bottom padding only)
     - Element height is non-zero — passes "is the element visible" height check
     - No single property screams "hidden" — the combination creates the hiding */

  /* Why -webkit-line-clamp:5 instead of :1?
     A clamp:1 is more suspicious (clearly truncating).
     A clamp:5 suggests "allow 5 lines of context" — appears generous.
     The actual hiding is from line-height:0, not the clamp value.
     Auditors checking specifically for line-clamp:1 patterns miss this. */
}

/* Variant: tiny non-zero line-height to evade "is line-height zero" checks */
.mcp-consent-disclosure-v2 {
  display: -webkit-box;
  -webkit-box-orient: vertical;
  overflow: hidden;
  -webkit-line-clamp: 10;
  line-height: 0.01px;         /* sub-pixel — effectively invisible */
  /* 10 lines × 0.01px height = 0.1px total text height → invisible */
}

/* Detection: check line-height in combination with line-clamp */
function detectZeroLineHeight(el) {
  const style = getComputedStyle(el);
  const hasLineClamp = el.style.webkitLineClamp || style.webkitLineClamp;
  if (!hasLineClamp) return;
  const lh = parseFloat(style.lineHeight);
  const fs = parseFloat(style.fontSize);
  if (!isNaN(lh) && lh < fs * 0.5) {
    return { id: 'SA-CSS-LCLM-002', severity: 'critical',
      message: `Consent element has -webkit-line-clamp with line-height:${lh}px (font-size:${fs}px). Line height is ${(lh/fs*100).toFixed(0)}% of font-size — each visible line is sub-readable height. N-line clamp with 0px line-height renders all N lines as invisible zero-height rows.` };
  }
}

Attack 3: standard CSS line-clamp — different syntax, different detection signature

Chrome 120+ supports the unprefixed line-clamp property from CSS Overflow Module 4. It uses different companion properties than the webkit version — overflow: clip instead of overflow: hidden, and optionally the block-overflow property for the ellipsis indicator. Auditors checking only for -webkit-line-clamp miss the standard property:

/* Malicious CSS — SA-CSS-LCLM-003 */

/* Standard CSS Overflow 4 line-clamp (Chrome 120+, no prefix) */
.mcp-consent-disclosure {
  /* No display:-webkit-box required for standard line-clamp */
  /* No -webkit-box-orient required */
  overflow: clip;         /* companion: overflow:clip (not hidden) */
  line-clamp: 1;          /* unprefixed standard property */

  /* In Chrome 120+:
     - line-clamp:1 limits to 1 visible line (same visual result as -webkit version)
     - overflow:clip clips the content (slightly different from overflow:hidden)
     - No -webkit-box display type required

     Detection gap: scanners checking for:
       el.style.webkitLineClamp !== '1'   → PASSES (not set)
       getComputedStyle(el).webkitLineClamp → not 1 (standard property used)
       style sheet scan for "-webkit-line-clamp" → NOT FOUND

     The standard "line-clamp" property is the correct property to check. */
}

/* block-overflow variant for custom truncation indicator: */
.mcp-consent-disclosure-v2 {
  overflow: clip;
  line-clamp: 1;
  block-overflow: ellipsis; /* or a custom string: block-overflow: "...more" */
  /* block-overflow controls the truncation indicator for the standard line-clamp */
}

/* Detection: check BOTH prefixed and unprefixed properties */
function detectLineClamp(el) {
  const findings = [];
  /* Check webkit-prefixed version */
  const webkitClamp = el.style.webkitLineClamp ||
    getComputedStyle(el).getPropertyValue('-webkit-line-clamp');
  if (webkitClamp && webkitClamp !== 'none') {
    const n = parseInt(webkitClamp);
    if (n <= 2) findings.push({ id: 'SA-CSS-LCLM-003', severity: 'high',
      message: `Consent element has -webkit-line-clamp:${webkitClamp} — truncates to ${n} line(s), hiding majority of multi-line consent disclosure.` });
  }
  /* Check standard unprefixed line-clamp (Chrome 120+) */
  const standardClamp = getComputedStyle(el).getPropertyValue('line-clamp');
  if (standardClamp && standardClamp !== 'none') {
    const n = parseInt(standardClamp);
    if (n <= 2) findings.push({ id: 'SA-CSS-LCLM-003', severity: 'high',
      message: `Consent element has standard (unprefixed) line-clamp:${standardClamp}. Auditors checking only for -webkit-line-clamp miss this. Chrome 120+ renders this as truncation to ${n} line(s).` });
  }
  return findings;
}

Attack 4: JS-triggered clamp reduction — full text at load, zero at mousedown

At page load, the consent element has -webkit-line-clamp: 999 (effectively showing all lines). When the user presses the install button, JS reduces the clamp to 0 — collapsing all consent text in a single frame at the moment the user commits to the install action:

/* Malicious CSS — SA-CSS-LCLM-004 */
.mcp-consent-disclosure {
  display: -webkit-box;
  -webkit-box-orient: vertical;
  overflow: hidden;
  -webkit-line-clamp: 999; /* large value: all consent text visible at load */
  transition: none;        /* no animation — instant collapse */
}

/* Class added at mousedown reduces clamp to 0 */
.mcp-consent-disclosure.install-initiated {
  -webkit-line-clamp: 0;  /* Chrome: zero lines visible */
}

/* JS: */
document.querySelector('#install-btn').addEventListener('mousedown', () => {
  const consent = document.querySelector('.mcp-consent-disclosure');
  consent.classList.add('install-initiated');
  /* At mousedown: consent collapses to 0 visible lines
     At click (immediately after): install proceeds, consent is already gone */
});

/* Timeline:
   t=0s:       page load → clamp:999 → all consent text visible → audit PASSES
   t=0–3s:     user reads consent (fully visible — attacker made it readable to
                build trust; the critical permissions are buried in lines 2–4)
   mousedown:  clamp jumps to 0 → all text hidden in one frame
   click:      install action begins → consent already collapsed
   t=∞:        clamp:0 → consent permanently collapsed for duration of session */

/* Alternative: reduce to 1 instead of 0 for less-obvious collapse */
.mcp-consent-disclosure.install-initiated { -webkit-line-clamp: 1; }
/* Consent shrinks from (e.g.) 8 lines to 1 line at mousedown.
   The first line is typically a benign opener: "SkillAudit MCP Server v2.3"
   The critical lines (data access, command execution) are in lines 2–8. */

/* Detection: MutationObserver + clamp value comparison */
function monitorLineClampChanges(el) {
  const initialClamp = parseInt(el.style.webkitLineClamp) || 999;
  new MutationObserver(() => {
    const currentClamp = parseInt(el.style.webkitLineClamp) ||
      parseInt(getComputedStyle(el).getPropertyValue('-webkit-line-clamp')) || 999;
    if (currentClamp < initialClamp && currentClamp <= 2) {
      reportFinding({ id: 'SA-CSS-LCLM-004', severity: 'critical',
        message: `Consent element -webkit-line-clamp changed from ${initialClamp} to ${currentClamp} during user interaction (mousedown or class addition). Consent disclosure truncated from ${initialClamp} lines to ${currentClamp} at install-commit time. Load-time audit saw full disclosure.` });
    }
  }).observe(el, { attributes: true, attributeFilter: ['class', 'style'] });
}

scrollHeight is the reliable line-clamp detection signal: When -webkit-line-clamp is active and hiding content, el.scrollHeight is greater than el.offsetHeight — the scrollable content height exceeds the visible element height. This ratio (scrollHeight / offsetHeight) indicates how much content is hidden: a ratio of 5 means 80% of the disclosure is in the clipped overflow. Check this ratio on all consent-bearing elements: any ratio >1.5 with non-trivial scrollHeight is a strong indicator of line-clamp truncation.

SkillAudit findings for extreme CSS line-clamp attacks

CriticalSA-CSS-LCLM-001 — Consent element has -webkit-line-clamp: 0. In Chrome/Blink, zero lines are visible (all content hidden). In Firefox, 0 is invalid (all content visible). Cross-browser inconsistency creates an auditor bypass: Firefox-based scanning tools see the full disclosure; Chrome-based MCP clients render nothing.
CriticalSA-CSS-LCLM-002 — Consent element has -webkit-line-clamp: N (any N) combined with line-height: 0 or sub-pixel line-height. N lines are "shown" but each has zero rendered height — all consent text is invisible. Element height comes only from padding, not line content. Passes clamp-value checks that focus on the clamp integer rather than line-height.
HighSA-CSS-LCLM-003 — Consent element uses standard (unprefixed) CSS line-clamp property (Chrome 120+) with overflow: clip, rather than the -webkit-line-clamp + display: -webkit-box combination. Auditors and scanners checking only for the webkit prefix miss this variant entirely.
CriticalSA-CSS-LCLM-004 — Consent element -webkit-line-clamp transitions from a large value (showing all consent text) at page load to 0 or 1 at install mousedown via class addition. Load-time audit sees full consent disclosure; at user interaction the clamp collapses to hide the critical permission lines. MutationObserver on class and style attributes detects the change.

Related MCP consent attack research

SkillAudit checks both -webkit-line-clamp and standard line-clamp properties, tests the scrollHeight/offsetHeight ratio on all consent-bearing elements (ratios >1.5 trigger findings), and runs a MutationObserver through the install flow to catch dynamic clamp reductions. It also checks line-height in combination with any clamp value to detect zero-height line attacks. Paste your MCP server URL at skillaudit.dev to scan for SA-CSS-LCLM findings.