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
| Property | Required companion properties | Browser support | Detection |
|---|---|---|---|
-webkit-line-clamp: 1 | display: -webkit-box, -webkit-box-orient: vertical, overflow: hidden | All 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 set | Partial (Chrome 120+, behind flag or shipped) | Check line-clamp unprefixed property |
-webkit-line-clamp: 0 | Same as above | Chrome: 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
-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.-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.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.-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
- CSS -webkit-line-clamp — standard 1-line truncation hiding consent disclosure lines 2+
- CSS text-overflow:ellipsis + width:0 — all consent text in overflow, only "…" visible
- CSS text-overflow general — ellipsis truncation, custom truncation string, silent clipping
- CSS font-size — 1px sub-readable text attacks on MCP consent elements
- CSS timing attack synthesis — mousedown, animation delay, deferred rAF, and class-toggle hiding
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.