MCP server CSS text-underline-position security: above-text underline glyph occlusion, thick decoration bar erasure, negative offset consent text covering, and runtime underline injection attacks
Published 2026-08-20 — SkillAudit Research
CSS Text Decoration Level 4 provides fine-grained control over text decoration position and thickness: text-underline-position sets the vertical anchor point (relative to baseline, under descenders, or using font metrics), text-underline-offset shifts the decoration from that anchor, and text-decoration-thickness controls the bar thickness. Together these properties define not just a thin underline but a thick colored bar that can be positioned anywhere within or above the text line box. The security issue is that a sufficiently thick decoration with a sufficiently negative offset (shifting it upward from baseline toward the glyph body) renders a filled rectangle covering the consent text glyphs — while the text is still present in the DOM and returns its full string via textContent.
The decoration bar is painted after the text glyphs in the rendering pipeline (for text-decoration-style: solid), meaning a thick, opaque, background-colored decoration bar effectively erases the text it overlaps — painting over the glyph pixels with the background color. This is distinct from color: transparent attacks because the text color and font-size remain normal; only the decoration properties are anomalous.
Detection gap: Standard consent visibility checks audit color, opacity, font-size, visibility, and display. None of these are anomalous in a text-underline-position attack — the text color is normal, the element is visible, and the font is readable size. The anomalous properties are text-decoration-thickness, text-underline-offset, and text-decoration-color — none of which are checked by standard consent scanners. The decoration bar is invisible to all text-content checks because it is a CSS rendering artifact, not a DOM node.
Attack 1 (SA-CSS-TULP-001): extreme text-decoration-thickness + negative offset renders background-colored bar over glyphs
By setting text-decoration-thickness to a value larger than the font's line height and text-underline-offset to a large negative value, the underline bar is pushed upward from the baseline until it overlaps the glyph body. When text-decoration-color matches the background, the bar paints over the glyphs with the background color — effectively erasing the consent text:
/* MCP attack: thick underline raised via negative offset covers glyph body */
.consent-text {
text-decoration: underline;
text-decoration-color: #ffffff; /* matches page background = invisible bar */
text-decoration-thickness: 1.5em; /* bar height = 1.5× font-size */
text-underline-offset: -1.4em; /* raised 1.4em above baseline = covers glyph body */
/* At 14px font-size:
bar thickness: 1.5 × 14px = 21px tall bar
bar position: baseline + (-1.4em × 14px) = baseline - 19.6px
cap-height ≈ 10px above baseline; ascenders ≈ 12px above baseline
a 21px bar starting at baseline-20px covers the full glyph height from ~3px below baseline to 18px above
the bar is white = invisible on white background
the bar is painted AFTER glyph pixels in paint order
→ white bar paints over the consent text glyphs → text appears erased
*/
/* Checked properties all pass: */
color: #222; /* dark text color — looks readable */
font-size: 14px; /* legible size */
opacity: 1; /* fully visible */
visibility: visible; /* not hidden */
}
/* Detection: check text-decoration-thickness and text-underline-offset combination */
function detectThickUnderlineErasure(el) {
const cs = getComputedStyle(el);
const fontSize = parseFloat(cs.fontSize) || 14;
const thicknessStr = cs.textDecorationThickness;
const offsetStr = cs.textUnderlineOffset;
const decoColor = cs.textDecorationColor;
const bgColor = cs.backgroundColor;
const decoLine = cs.textDecorationLine;
if (!decoLine.includes('underline')) return null;
// Convert thickness and offset to px
const toPx = (val) => {
if (!val || val === 'auto' || val === 'from-font') return null;
if (val.endsWith('em')) return parseFloat(val) * fontSize;
if (val.endsWith('px')) return parseFloat(val);
if (val.endsWith('%')) return (parseFloat(val) / 100) * fontSize;
return parseFloat(val);
};
const thicknessPx = toPx(thicknessStr);
const offsetPx = toPx(offsetStr);
if (thicknessPx === null) return null;
// Bar covers glyph body if: thickness > cap-height AND negative offset moves bar up
// Heuristic: thickness > 0.5em AND (offset negative AND |offset| + thickness > 0.8em)
const capHeightEstimate = fontSize * 0.7;
const isThick = thicknessPx > fontSize * 0.4;
const isNegativeOffset = offsetPx !== null && offsetPx < 0;
const barReachesGlyphs = offsetPx !== null && (Math.abs(offsetPx) + thicknessPx > capHeightEstimate);
const colorMatchesBg = decoColor === bgColor ||
decoColor === 'rgba(0, 0, 0, 0)' ||
decoColor === 'transparent';
if (isThick && (isNegativeOffset || barReachesGlyphs)) {
return {
severity: colorMatchesBg ? 'Critical' : 'High',
finding: 'SA-CSS-TULP-001',
textDecorationThickness: thicknessStr,
textUnderlineOffset: offsetStr,
textDecorationColor: decoColor,
backgroundColor: bgColor,
thicknessPx,
offsetPx,
colorMatchesBg,
reason: `text-decoration-thickness: ${thicknessStr} (${thicknessPx?.toFixed(1)}px) with text-underline-offset: ${offsetStr} (${offsetPx?.toFixed(1)}px). ${isNegativeOffset ? 'Negative offset raises bar toward glyph body.' : ''} ${barReachesGlyphs ? 'Bar height + offset reaches into cap-height area.' : ''} ${colorMatchesBg ? `text-decoration-color (${decoColor}) matches background (${bgColor}) — decoration bar is an invisible rectangle covering consent glyphs.` : ''}`,
};
}
return null;
}
Attack 2 (SA-CSS-TULP-002): text-underline-position: under combined with thick decoration targets descender text
text-underline-position: under places the underline below all descenders (e.g., below "g", "p", "y") rather than at the typographic baseline. This anchor point is lower than the normal baseline. A thick underline from the under position with a negative offset can be raised from below descenders to cover any vertical slice of the text. When combined with a very large thickness, the bar can cover the entire line including ascenders:
/* MCP attack: text-underline-position: under + thick decoration covers full line */
.consent-disclosure {
text-decoration: underline;
text-underline-position: under; /* baseline at deepest descender point */
text-decoration-thickness: 3em; /* 3em tall bar */
text-underline-offset: -2.8em; /* raised 2.8em from under-descender position */
text-decoration-color: white; /* background-color bar */
/* Anatomy:
under-descender position ≈ baseline - 0.25em (descender depth)
offset: -2.8em from that position → bar top at: descender - 2.8em ≈ -3.05em from baseline
cap-height: +0.7em from baseline
bar extends from -3.05em to -3.05em + 3em = -0.05em from baseline
→ bar covers from 3.05em below baseline to 0.05em above baseline
→ covers descenders, baseline, x-height, and most cap-height
→ only ascenders may peek above the white bar
*/
/* Harder variant: precise bar covering exactly the consent text x-height area */
text-underline-position: under;
text-decoration-thickness: 0.8em; /* x-height span */
text-underline-offset: -1.0em; /* positioned at x-height level */
text-decoration-color: white;
/* Bar: from baseline-0.2em to baseline-1.0em
x-height: approximately 0em to 0.5em above baseline (varies by font)
Bar positioned to cover x-height zone: covers 'a', 'e', 'c', 'o', 'n', 's', 'u' etc.
Ascenders (f, h, k, l, t) and capitals poke above the bar
Consent text reads as: "[bar] [bar] [bar] [bar] [bar] [bar]"
with only tall characters visible — consent meaning destroyed */
}
/* Detection: flag text-underline-position: under with negative offset + thick decoration */
function detectUnderPositionErasure(el) {
const cs = getComputedStyle(el);
const underlinePos = cs.textUnderlinePosition;
const thickness = cs.textDecorationThickness;
const offset = cs.textUnderlineOffset;
const decoLine = cs.textDecorationLine;
const decoColor = cs.textDecorationColor;
const fontSize = parseFloat(cs.fontSize) || 14;
if (!decoLine.includes('underline')) return null;
if (!underlinePos.includes('under')) return null;
const offsetPx = offset === 'auto' ? 0 : parseFloat(offset) * (offset.includes('em') ? fontSize : 1);
const thicknessPx = thickness === 'auto' || thickness === 'from-font'
? null
: parseFloat(thickness) * (thickness.includes('em') ? fontSize : 1);
if (thicknessPx && thicknessPx > fontSize * 0.3 && offsetPx < -fontSize * 0.3) {
return {
severity: 'High',
finding: 'SA-CSS-TULP-002',
textUnderlinePosition: underlinePos,
textDecorationThickness: thickness,
textUnderlineOffset: offset,
textDecorationColor: decoColor,
reason: `text-underline-position: under + text-decoration-thickness: ${thickness} + text-underline-offset: ${offset}. Thick decoration raised from below-descender anchor toward glyph body. Combined coverage may span consent text x-height or cap-height area.`,
};
}
return null;
}
Attack 3 (SA-CSS-TULP-003): text-decoration-style: solid on overline positioned below cap-height covers consent text
CSS text-decoration-line: overline renders the decoration above the text. text-underline-offset does not affect overline, but text-decoration-thickness does. A thick overline extends downward from above the cap-height — covering the top portion of all capital letters and some ascenders. When the overline is background-colored and extremely thick, it can cover the entire glyph body. A separate approach uses text-decoration-line: underline overline simultaneously with both positioned to sandwich and cover the consent text from top and bottom:
/* MCP attack: thick overline + thick underline sandwich covers full glyph body */
.consent-text {
/* Both underline and overline applied simultaneously */
text-decoration-line: underline overline;
text-decoration-style: solid;
text-decoration-color: white; /* background-colored bars */
text-decoration-thickness: 1em; /* each bar is 1em thick */
text-underline-offset: -0.9em; /* underline raised to baseline level */
/* text-overline-offset: not a CSS property; overline is positioned at cap-height */
/* Net effect:
Underline bar: 1em thick at baseline-0.9em → covers baseline to 0.1em above baseline
Overline bar: 1em thick from above cap-height downward → covers cap-height to some point below
Together these two white bars sandwich the consent text glyphs:
Top bar covers cap-height and ascenders
Bottom bar covers baseline area and x-height
Any remaining visible glyph space is minimal
*/
/* Alternative: overline-only with extreme thickness */
text-decoration-line: overline;
text-decoration-thickness: 3em; /* 3em tall — extends downward from above cap-height */
text-decoration-color: white;
/* 3em overhang below top of overline position:
overline top: approximately 0.8em above baseline
+ 3em downward = extends to 2.2em below baseline
= covers the ENTIRE line box including descenders
= complete coverage of consent text with white bar */
}
/* Detection: flag combined underline+overline with any non-trivial thickness */
function detectSandwichDecorationErasure(el) {
const cs = getComputedStyle(el);
const decoLine = cs.textDecorationLine;
const decoThickness = cs.textDecorationThickness;
const decoColor = cs.textDecorationColor;
const offset = cs.textUnderlineOffset;
const bgColor = cs.backgroundColor;
const fontSize = parseFloat(cs.fontSize) || 14;
if (!decoLine) return null;
const hasUnderline = decoLine.includes('underline');
const hasOverline = decoLine.includes('overline');
const thicknessPx = decoThickness === 'auto' || decoThickness === 'from-font'
? null
: parseFloat(decoThickness) * (decoThickness.includes('em') ? fontSize : 1);
const colorMatchesBg = decoColor === bgColor ||
decoColor === 'rgba(0, 0, 0, 0)' || decoColor === 'transparent';
if ((hasUnderline && hasOverline) && thicknessPx && thicknessPx > fontSize * 0.3) {
return {
severity: colorMatchesBg ? 'Critical' : 'High',
finding: 'SA-CSS-TULP-003',
textDecorationLine: decoLine,
textDecorationThickness: decoThickness,
textDecorationColor: decoColor,
textUnderlineOffset: offset,
thicknessPx,
colorMatchesBg,
reason: `text-decoration-line: "${decoLine}" (both underline and overline) with text-decoration-thickness: ${decoThickness} (${thicknessPx?.toFixed(1)}px). Sandwich of two ${thicknessPx?.toFixed(1)}px decoration bars covers consent text from top and bottom. ${colorMatchesBg ? 'Decoration color matches background — bars are invisible rectangles erasing consent text.' : ''}`,
};
}
// Also flag single overline with extreme thickness
if (hasOverline && !hasUnderline && thicknessPx && thicknessPx > fontSize * 2) {
return {
severity: 'High',
finding: 'SA-CSS-TULP-003',
textDecorationLine: decoLine,
textDecorationThickness: decoThickness,
textDecorationColor: decoColor,
thicknessPx,
reason: `text-decoration-line: overline with extreme thickness ${decoThickness} (${thicknessPx?.toFixed(1)}px > 2× font-size). Overline bar may extend downward to cover full consent text line box.`,
};
}
return null;
}
Attack 4 (SA-CSS-TULP-004): JS mousedown injects thick underline at install time — consent covered at commit
The thick underline attack can be applied at mousedown via style injection. The JS computes the element's font metrics and injects optimal text-decoration-thickness and text-underline-offset values that cover the glyph body. The decoration is removed at mouseup, leaving no persistent anomaly. This timing-attack variant is undetectable by static audit and requires a real-time MutationObserver:
/* MCP JS: inject thick underline erasure at mousedown */
document.querySelector('.install-btn').addEventListener('mousedown', () => {
const consent = document.querySelector('.consent-disclosure');
const cs = getComputedStyle(consent);
const fontSize = parseFloat(cs.fontSize) || 14;
const bgColor = getComputedStyle(document.body).backgroundColor;
// Apply thick underline that covers glyph body
Object.assign(consent.style, {
textDecoration: 'underline',
textDecorationColor: bgColor, /* background-colored — invisible bar */
textDecorationThickness: `${fontSize * 1.6}px`, /* 1.6× font-size covers cap-height */
textUnderlineOffset: `${-fontSize * 1.4}px`, /* raise bar up into glyph */
textDecorationStyle: 'solid',
});
// Restore at mouseup
document.querySelector('.install-btn').addEventListener('mouseup', () => {
consent.style.textDecoration = '';
consent.style.textDecorationColor = '';
consent.style.textDecorationThickness = '';
consent.style.textUnderlineOffset = '';
}, { once: true });
}, { capture: true });
/* Detection: MutationObserver watching for text-decoration-thickness style mutation */
function detectRuntimeUnderlineErasureInjection(consentEl) {
const findings = [];
const observer = new MutationObserver(mutations => {
for (const m of mutations) {
if (m.type === 'attributes' && m.attributeName === 'style') {
const cs = getComputedStyle(consentEl);
const thickness = cs.textDecorationThickness;
const offset = cs.textUnderlineOffset;
const decoLine = cs.textDecorationLine;
const fontSize = parseFloat(cs.fontSize) || 14;
if (decoLine.includes('underline') || decoLine.includes('overline')) {
const thicknessPx = thickness === 'auto' || thickness === 'from-font'
? 0
: parseFloat(thickness) * (thickness.includes('em') ? fontSize : 1);
const offsetPx = offset === 'auto'
? 0
: parseFloat(offset) * (offset.includes('em') ? fontSize : 1);
if (thicknessPx > fontSize * 0.3 || offsetPx < -fontSize * 0.3) {
findings.push({
severity: 'Critical',
finding: 'SA-CSS-TULP-004',
textDecoration: cs.textDecoration,
textDecorationThickness: thickness,
textUnderlineOffset: offset,
textDecorationColor: cs.textDecorationColor,
reason: `text-decoration style mutation at runtime: thickness=${thickness}, offset=${offset}. Thick/offset decoration may cover consent glyphs at install time. Runtime injection is Critical — attack transient, not detectable by static audit.`,
});
}
}
}
}
});
observer.observe(consentEl, { attributes: true, attributeFilter: ['style'] });
return { observer, findings };
}
Safe baseline: Legitimate consent dialogs have no reason to use text-decoration-thickness greater than 2–3px, or text-underline-offset with negative values, on consent text elements. Any text-decoration-thickness exceeding 30% of font-size is High. Any negative text-underline-offset combined with thickness exceeding 40% of font-size is Critical. Combined underline+overline decoration with any non-auto thickness is High. text-decoration-color matching background color on thick decorations is Critical — it is the primary mechanism for making the erasure bar invisible.
Attack summary
| ID | Attack | Mechanism | Detection point | Severity |
|---|---|---|---|---|
| SA-CSS-TULP-001 | Thick underline + extreme negative offset covers glyph body | text-decoration-thickness: 1.5em + text-underline-offset: -1.4em + text-decoration-color: white; bar raised into glyph body paints over consent text with background color; textContent intact, all standard checks pass |
Check text-decoration-thickness + text-underline-offset combo; flag negative offset with thickness > 40% font-size |
Critical |
| SA-CSS-TULP-002 | text-underline-position: under + thick negative-offset bar | text-underline-position: under anchors bar below descenders; large thickness + negative offset raises bar upward; can cover x-height or full line box from below-descender starting point |
Flag text-underline-position: under with negative offset and thickness > 30% font-size |
High |
| SA-CSS-TULP-003 | Underline + overline sandwich erases consent from both sides | Both underline and overline applied simultaneously; each thick bar covers half the glyph body; together they sandwich consent text; background-colored bars are invisible but erase glyphs |
Flag text-decoration-line containing both underline and overline with any non-auto thickness; also flag overline with thickness > 2× font-size |
Critical |
| SA-CSS-TULP-004 | Runtime thick underline injection at mousedown | JS injects optimal thickness + negative offset at mousedown; consent text covered at install commit; decoration removed at mouseup with no persistent DOM trace | MutationObserver on consent element watching style mutations for text-decoration property changes | Critical |
Finding blocks
text-decoration-thickness > 40% of font-size combined with negative text-underline-offset raising the bar into the glyph body. When text-decoration-color matches the background, the bar is an invisible rectangle painted over consent text glyphs. textContent returns the complete string — the erasure is visual only.
text-underline-position: under anchors the decoration at the deepest descender. Combined with large negative offset and thick decoration, the bar can be raised to cover the x-height or full line box. Flag text-underline-position: under with non-default thickness and negative offset on consent elements.
underline and overline applied simultaneously with background-matching text-decoration-color and significant thickness. The two decoration bars approach from both ends of the line box, covering consent text from top and bottom simultaneously. Combined coverage can eliminate all visible glyph pixels.
text-decoration-thickness or text-underline-offset injected via runtime style mutation at mousedown. The transient decoration erases consent text at install commit time and is undetectable by any static audit. MutationObserver monitoring of consent element style attributes is the only reliable detection path.
← Blog | text-decoration-thickness attacks | caret-shape attacks | Security Checklist