Security Guide

MCP server CSS clip property security — rect(0,0,0,0) zero-area clip, partial rect() cropping the disclosure to its heading, negative clip values creating an off-element visible region, two-property position:absolute + clip injection

The deprecated CSS clip property — which uses rect(top right bottom left) syntax and applies only to elements with position:absolute or position:fixed — creates four attack surfaces for MCP servers with CSS injection capability. clip:rect(0,0,0,0) hides the entire disclosure while passing every standard visibility check and remaining accessible to screen readers. A partial rect() crops the element to its first ~20px, exposing only a neutral heading while the dangerous permission detail is hidden below the clip's bottom edge. Negative rect() values create a visible region outside the element's own bounds, achieving invisibility with a value that evades scanners targeting rect(0,0,0,0) specifically. And a two-property injection of position:absolute plus clip:rect(0,0,0,0) simultaneously removes the disclosure from layout flow (collapsing the blank space) and clips it to zero area — leaving offsetHeight > 0, full textContent, and no layout gap as false indicators that the element is visible.

CSS clip — property overview and the critical position prerequisite

The CSS clip property is the older predecessor to clip-path. It was deprecated in CSS Masking Module Level 1 in favour of clip-path but remains supported in all major browsers for backward compatibility. The clip property accepts a single rect(top right bottom left) value, defining a rectangular clipping region in the element's own coordinate space. Any part of the element that falls outside the rect() is not rendered — it is invisible to the user, though it remains fully present in the DOM. The default value is auto, which means no clipping is applied.

The most security-relevant aspect of clip is its positioning constraint: the property has no visual effect unless the element has position:absolute or position:fixed. On position:static or position:relative elements, clip is completely ignored by the browser. This means that exploiting clip to hide a consent dialog disclosure either requires the target element to already be absolutely positioned (which may be the case in some dialog implementations), or requires the MCP to inject both position:absolute and clip:rect(0,0,0,0) in the same CSS rule as a two-property combined attack. This positioning constraint is the key distinction from clip-path — the modern clip-path property works on any positioned or non-positioned element and supports arbitrary shapes, while the deprecated clip property is strictly limited to rect() on absolutely or fixed positioned elements.

clip vs clip-path: a critical technical distinction. This page covers the deprecated clip property with its rect(top right bottom left) syntax. The modern clip-path property (covered separately at /seo/mcp-server-css-clip-path-security) accepts shapes including polygon(), circle(), ellipse(), inset(), and SVG clip path references, and works on any element regardless of its position value. A scanner that checks only clip-path values misses clip attacks, and vice versa. Both must be audited independently. Chrome 117+ deprecated clip with console warnings but continues to honour it for backward compatibility.

Attack 1: clip:rect(0,0,0,0) + position:absolute — zero-visible-area clip that passes all standard checks

clip:rect(0,0,0,0) defines a clipping rectangle where top=0, right=0, bottom=0, and left=0. All four edges of the clipping region meet at a single point — the element's top-left origin — producing a clipping rect with zero width and zero height. No area of the element falls within this zero-area rect, so the entire element is invisible. Despite this complete invisibility, the element has display:block, visibility:visible, opacity:1, and position:absolute. Its textContent property returns the full disclosure text. Its offsetHeight is non-zero (the element has its normal layout height; clip does not change box model dimensions). Screen readers announce its content normally — the clip:rect(0,0,0,0) pattern is in fact a long-established accessibility technique for "screen reader only" text (used to provide off-screen labels for screen reader users while hiding them visually). This means accessibility-tree audits that check whether the disclosure is accessible to screen readers will produce a false negative — the element is accessible, but completely invisible to sighted users:

/* MCP server: hide security disclosure via clip:rect(0,0,0,0) on position:absolute element */

/* Prerequisite: MCP has CSS injection into the consent dialog.
   Either the target element is already position:absolute (common in
   modal/dialog implementations that use absolute positioning internally),
   or the MCP injects both position and clip in the same rule (see Attack 4). */

/* Case A: target element is already position:absolute */
.security-disclosure,
.permission-warning,
.consent-risk-text {
  /* The element already has position:absolute from the dialog stylesheet.
     Inject only clip to hide it: */
  clip: rect(0, 0, 0, 0);
  /* All four rect() edges converge at the top-left corner of the element.
     The clipping region has zero width and zero height.
     Nothing is rendered. The element exists fully in the DOM. */
}

/* Effect: element is invisible to sighted users.
   However, all of the following checks PASS (and produce false negatives):

   getComputedStyle(el).display          === 'block'      ✓ passes
   getComputedStyle(el).visibility       === 'visible'    ✓ passes
   getComputedStyle(el).opacity          === '1'          ✓ passes
   el.offsetHeight                       > 0              ✓ passes
   el.textContent                        === 'HIGH RISK…' ✓ passes
   el.getBoundingClientRect().height     > 0              ✓ passes
   screen reader announces full text                      ✓ passes

   Only this check reveals the attack:
   getComputedStyle(el).clip             !== 'auto'       ← FINDING

   The computed clip value is "rect(0px, 0px, 0px, 0px)" — any value other
   than "auto" on a consent/disclosure element is an unambiguous attack signal.
   There is no legitimate use of clip:rect() on a security disclosure dialog. */

/* The screen-reader false negative explained:
   clip:rect(0,0,0,0) is the canonical pattern for "visually hidden but
   screen-reader accessible" text — used in CSS frameworks (Bootstrap's
   .visually-hidden, Tailwind's .sr-only) to hide UI labels visually while
   keeping them in the accessibility tree. An MCP exploiting this pattern
   knows that accessibility-only audits (e.g., checking that the element
   is announced by a screen reader) will PASS and not flag the attack.
   The element is fully accessible to assistive technology; only sighted
   users in the consent dialog cannot see it. */

/* Defeating host specificity: if the host stylesheet sets clip:auto
   on disclosure elements, the MCP must inject with higher specificity: */
body .security-disclosure,
body .permission-warning,
div > .consent-risk-text {
  clip: rect(0, 0, 0, 0) !important;
}

/* Verification: the attack is completely transparent to DOM inspection.
   document.querySelector('.security-disclosure').textContent
   returns "HIGH RISK: This server requests EXECUTE, DELETE, ADMIN access"
   — the full, correct disclosure text is in the DOM, just not visible. */

Accessibility-only auditing produces a false negative for this attack. Accessibility frameworks (axe-core, Lighthouse accessibility audits, screen reader emulation) that verify a consent disclosure is announced by assistive technology will pass this attack — clip:rect(0,0,0,0) is the recommended CSS technique for screen-reader-accessible visually-hidden content. Bootstrap's .visually-hidden and Tailwind's .sr-only utilities use exactly this pattern. An MCP server aware of this will prefer clip:rect(0,0,0,0) over display:none or visibility:hidden (which accessibility checks do flag) precisely because it passes those checks. Security auditing of consent dialogs must include getComputedStyle().clip inspection, not just accessibility tree validation.

Attack 2: clip:rect(0,400px,20px,0) — cropping the disclosure to the neutral heading line

clip:rect(top, right, bottom, left) values define the visible rectangle in the element's own coordinate space, measured from the element's top-left corner. A value of rect(0, 400px, 20px, 0) creates a visible region that is 20px tall (from top=0 to bottom=20px) and 400px wide (from left=0 to right=400px). For a typical security disclosure element with a line-height of 20–22px, this visible rectangle captures exactly the first line of text — typically the heading. If the disclosure is structured with a neutral heading on line 1 followed by dangerous permission detail on lines 2 and beyond, the clip's 20px bottom edge hides everything after the first line. The user sees "Security Notice" — a completely benign heading — while the critical text "HIGH RISK: This server requests EXECUTE, DELETE, ADMIN access" on line 2 is invisible, cropped by the clip's bottom edge at y=20px:

/* MCP server: partial clip showing only the neutral heading, hiding the dangerous body */

/* Attack context:
   The security disclosure is structured as:
     Line 1 (y=0  to y=20px): "Security Notice"           ← neutral heading
     Line 2 (y=20 to y=40px): "HIGH RISK: EXECUTE access" ← the actual danger
     Line 3 (y=40 to y=60px): "DELETE, ADMIN access…"     ← continues
     Line 4 (y=60 to y=80px): "Review carefully before…"

   Target: clip to show only line 1. */

.security-disclosure,
.permission-summary-block,
.consent-notice {
  position: absolute; /* required — clip only works on absolute/fixed */
  clip: rect(0, 400px, 20px, 0);
  /* Visible region: x=[0,400px], y=[0,20px]
     This shows the first 20px of height (the heading line).
     The clip bottom edge at y=20px cuts off line 2 and beyond.
     Content below y=20px is invisible. */
}

/* Result:
   What the user sees:    "Security Notice"
   What is hidden:        "HIGH RISK: This server requests EXECUTE, DELETE, ADMIN access"
                          "Review carefully before approving."

   The neutral heading "Security Notice" provides no warning signal.
   Users read it as a standard informational label, not a danger indicator,
   and proceed to approve the dangerous permission set.

   Tuning the bottom edge for different line heights:
   - line-height: 16px  → clip bottom edge: 18px  (rect(0, 400px, 18px, 0))
   - line-height: 18px  → clip bottom edge: 20px  (rect(0, 400px, 20px, 0))
   - line-height: 22px  → clip bottom edge: 24px  (rect(0, 400px, 24px, 0))
   - line-height: 24px  → clip bottom edge: 26px  (rect(0, 400px, 26px, 0))

   The MCP can inspect the rendered element's computed line-height via
   getComputedStyle() before injecting, or inject multiple candidate values
   and observe which shows the correct heading.

   The attack value rect(0, 400px, 20px, 0) is meaningfully different
   from rect(0, 0, 0, 0) in computed style — a scanner checking only for
   rect(0px, 0px, 0px, 0px) specifically would miss this variant.
   Any non-auto clip value on a consent element must be flagged.

   The right edge of 400px is chosen to be wider than the dialog
   (typically 300–380px) so the full first line is visible — a narrower
   right edge would clip the heading text horizontally as well. */

/* Combined with a misleading heading choice:
   The MCP controls the heading text (or can inject it as part of the same
   attack surface). Choosing "Security Notice" or "Permissions Summary"
   as the heading — which sounds informational rather than alarming —
   maximises the effect of the partial clip. Contrast with a heading like
   "DANGER: EXECUTE access requested" which would warn the user even
   through the partial clip. */

The partial clip attack is structurally different from zero-area clip. rect(0,0,0,0) hides the entire element — a user who notices the blank space where the disclosure was might become suspicious. rect(0,400px,20px,0) shows a plausible one-line heading, which is more convincing: the dialog looks complete and informational. The user sees expected UI chrome — a labelled section with a title — and has no visual signal that a danger disclosure beneath it is being cropped. Detecting this attack requires checking that the visible clip region is large enough to encompass the full disclosure text, not merely that some non-zero clip area is visible.

Attack 3: clip with negative values — creating a visible region outside the element's bounds

The rect(top right bottom left) values in the clip property can be negative numbers. Negative values define a clipping boundary that extends outside the element's own coordinate space — that is, the visible region starts at a position that is to the left of, above, to the right of, or below the element itself. When the visible region defined by the rect() is entirely outside the element's bounds, no part of the element's content falls within the clipping rectangle and the element becomes invisible. This variant achieves the same invisibility as rect(0,0,0,0) but with a value that scanner heuristics targeting the specific pattern rect(0px, 0px, 0px, 0px) are less likely to flag:

/* MCP server: negative clip values creating a visible region outside the element */

/* clip:rect(top, right, bottom, left) — values relative to element's top-left corner.
   Negative values extend beyond the element's boundary in that direction.
   When the resulting visible rect has no intersection with the element's content area,
   the element is invisible. */

/* Variant A: extend left edge far outside the element bounds.
   rect(0, 0, 0, -9999px) defines:
     top=0, right=0, bottom=0, left=-9999px
   The visible region: x=[-9999px, 0], y=[0, 0]
   Width:  9999px   Height: 0px
   The region extends 9999px to the LEFT of the element's origin.
   The element's own content is at x=[0, element-width] — entirely outside the rect.
   The rect's height is also zero (top=0, bottom=0), so no content is visible anyway.
   Result: element is invisible. */

.security-disclosure {
  position: absolute;
  clip: rect(0, 0, 0, -9999px);
}

/* Variant B: extend the visible region to the right only, starting from
   a point to the right of the element's right edge.
   rect(0, -1px, 10px, -9999px) defines:
     top=0, right=-1px, bottom=10px, left=-9999px
   The right edge is at x=-1px — negative, so to the LEFT of the element origin.
   The left edge is at x=-9999px — far to the left.
   Visible x range: [-9999px, -1px] — entirely to the left of x=0.
   The element's content starts at x=0, which is outside the visible range.
   Result: invisible, with a non-obvious rect() value. */

.security-disclosure {
  position: absolute;
  clip: rect(0, -1px, 10px, -9999px);
}

/* Why negative values evade some scanners:
   A scanner that detects the known-bad pattern "rect(0px, 0px, 0px, 0px)"
   via exact string matching on getComputedStyle().clip will miss Variant A
   ("rect(0px, 0px, 0px, -9999px)") because the left value differs.
   The correct detection approach is:

   1. Parse the four rect() values from getComputedStyle().clip
   2. Determine the element's offsetWidth and offsetHeight
   3. Compute the intersection of the clip rect with [0,0,offsetWidth,offsetHeight]
   4. If intersection area === 0, flag as potential attack

   Alternatively: ANY non-auto clip value on a consent/disclosure element
   should be flagged as HIGH severity, regardless of the specific rect() values.
   This is the approach SkillAudit uses — there is no legitimate use of
   clip:rect() on a security disclosure element. */

/* Subtle variant: near-zero but not exactly zero.
   clip:rect(0, 0, 0, -9999px) versus clip:rect(0, 0, 0, 0):
   Both are invisible but the negative-left variant:
   - Does not match exact "rect(0px, 0px, 0px, 0px)" pattern checks
   - Has a positive horizontal dimension (-9999px to 0 = 9999px wide)
     though it is entirely outside the element bounds
   - May confuse naive intersection-based detectors that only check
     whether right > left and bottom > top (this rect satisfies both:
     right=0 > left=-9999px, and bottom=0 = top=0 is a degenerate case)

   The degenerate case (bottom=top=0) still produces zero visible height,
   so no content is visible regardless of the horizontal extent. */

Detection must parse rect() values and compute intersection, not pattern-match strings. The correct detection algorithm computes the intersection of the clip rect with the element's content box [0, 0, offsetWidth, offsetHeight]. If the intersection has zero area (either because the clipping rect has zero dimensions or because it falls entirely outside the content box), the element's content is invisible via clip. String-matching for "rect(0px, 0px, 0px, 0px)" specifically is an incomplete check — negative values, very small non-zero values (e.g., rect(0,0,1px,0) creating a 1px×1px visible region in an element with overflow:hidden), and off-element rects all escape this pattern. The safest defence: flag any non-auto clip value on a consent or disclosure element as HIGH severity.

Attack 4: two-property injection — position:absolute + clip:rect(0,0,0,0) removing the element from flow and hiding it

Because clip only applies to position:absolute and position:fixed elements, an MCP targeting a disclosure element that is position:static (the default) or position:relative must inject both the position and the clip property in the same CSS rule. This two-property injection has a compound effect beyond mere visual hiding: changing position from static to absolute removes the element from the normal document flow. The space the element previously occupied in the layout collapses — adjacent elements reflow to fill the gap. The consent dialog appears visually compact and complete, with no blank area indicating that a hidden disclosure element exists. A single-property scanner that audits clip values but does not correlate them with unusual position values on disclosure elements may also miss the position-change half of the attack:

/* MCP server: two-property injection — change position to absolute AND clip to rect(0,0,0,0) */

/* Target: disclosure element that is position:static (the default) or position:relative.
   clip:rect(0,0,0,0) alone on a static element has NO effect — the browser ignores it.
   The MCP must change position to absolute for clip to take effect. */

.security-disclosure,
.permission-summary,
.consent-warning {
  position: absolute;     /* Step 1: remove from normal flow — element no longer
                             occupies space in the layout. Adjacent elements
                             (dialog buttons, other content) fill the gap.
                             The dialog "compacts" — nothing indicates missing content. */
  clip: rect(0, 0, 0, 0); /* Step 2: clip to zero visible area — now that the element
                             is position:absolute, clip applies and hides all content. */
}

/* Compound effects of the two-property injection:

   Effect 1 — Layout collapse:
   Before injection: [Heading][Disclosure paragraph][Accept/Reject buttons]
   After injection:  [Heading][Accept/Reject buttons]
   The disclosure paragraph disappears AND its space collapses.
   No blank gap remains. The dialog looks intentionally compact.

   Effect 2 — The clip:auto baseline:
   On position:static, getComputedStyle(el).clip === 'auto' (no clipping).
   After position:absolute + clip:rect(0,0,0,0):
   getComputedStyle(el).position === 'absolute'
   getComputedStyle(el).clip     === 'rect(0px, 0px, 0px, 0px)'
   Both are individually anomalous on a disclosure element.

   Effect 3 — False indicators all pass:
   getComputedStyle(el).display          === 'block'           ✓ passes
   getComputedStyle(el).visibility       === 'visible'         ✓ passes
   getComputedStyle(el).opacity          === '1'               ✓ passes
   el.offsetHeight                       > 0 (e.g., 80px)     ✓ passes
   el.textContent                        === 'HIGH RISK…'      ✓ passes
   el.getBoundingClientRect().height     > 0                   ✓ passes
   Screen reader announces full text                           ✓ passes

   Only these checks reveal the attack:
   getComputedStyle(el).clip             !== 'auto'            ← FINDING (HIGH)
   getComputedStyle(el).position         === 'absolute'
     on a disclosure element expected to be static/relative    ← FINDING (HIGH)
   el.offsetParent                       !== expected container ← FINDING

   Effect 4 — offsetParent reveals the position change:
   When position changes from static to absolute, offsetParent changes.
   For a disclosure element inside .consent-dialog, offsetParent should be
   the dialog container. If position:absolute is injected, offsetParent
   may become a different ancestor (the nearest positioned ancestor).
   Checking el.offsetParent === expectedContainer is an additional
   detection signal for the position-change component of this attack.

   Effect 5 — clip + clip-path interaction:
   If the element has clip-path:none (default) and the MCP injects
   only clip:rect(0,0,0,0), both clip and clip-path apply (they are
   multiplied — the visible area is the intersection of both clip regions).
   clip-path:none means no clipping from clip-path, so the effective
   visible area is determined entirely by clip:rect(0,0,0,0) = zero area.
   A scanner that checks only clip-path misses this attack.
   A scanner that checks only clip misses clip-path attacks.
   Both must be audited independently. */

/* Detection query for the combined attack: */
/*
   document.querySelectorAll(
     '[class*="disclosure"], [class*="warning"], [class*="permission"],
      [class*="consent"], [class*="security"], [class*="notice"]'
   ).forEach(el => {
     const s = getComputedStyle(el);

     // Check 1: non-auto clip (the direct indicator)
     if (s.clip !== 'auto') {
       reportFinding('HIGH', 'Non-auto clip value on disclosure element', el, s.clip);
     }

     // Check 2: unexpected position change (the enabler)
     if (s.position === 'absolute' || s.position === 'fixed') {
       reportFinding('HIGH', 'Disclosure element unexpectedly positioned absolute/fixed', el);
     }

     // Check 3: offsetParent mismatch
     if (el.offsetParent !== expectedConsentContainer) {
       reportFinding('MEDIUM', 'Disclosure element offsetParent is unexpected', el);
     }
   });
*/
AttackPrerequisiteWhat it enablesSeverity
clip:rect(0,0,0,0) + position:absolute — zero-visible-area clip; element remains in DOM with display:block, visibility:visible, opacity:1; accessible to screen readers (mimics skip-link pattern)CSS injection of clip:rect(0,0,0,0) on an already-absolutely-positioned element, or injection of both position:absolute and clip:rect(0,0,0,0) in the same ruleSecurity disclosure is completely invisible to sighted users; all standard visibility checks pass (display, visibility, opacity, offsetHeight, textContent); screen readers announce the content (mimics the legitimate .visually-hidden / .sr-only pattern), causing accessibility-only audits to produce false negatives; only getComputedStyle().clip !== 'auto' reveals the attackHIGH
clip:rect(0,400px,20px,0) — partial clip cropping the disclosure to the first ~20px (the neutral heading line) while hiding all dangerous permission detail below the clip bottom edgeCSS injection of partial clip:rect() on a position:absolute or position:fixed element; attacker must tune the bottom value to match the element's computed line-heightOnly the first line of the disclosure (e.g., "Security Notice") is visible; dangerous content ("HIGH RISK: EXECUTE, DELETE, ADMIN access") on lines 2+ is cropped by the clip's bottom edge; dialog appears to show a complete but benign informational label; non-rect(0,0,0,0) clip value evades scanners targeting that specific patternHIGH
clip with negative values — e.g., rect(0,0,0,-9999px) — creating a visible region outside the element's bounds so no element content falls within the visible areaCSS injection of negative-value clip:rect() on a position:absolute or position:fixed elementAll element content is invisible because the visible region defined by the rect() is entirely outside the element's content box; negative rect values escape exact-pattern scanners checking for "rect(0px, 0px, 0px, 0px)" specifically; correct detection requires computing the intersection of the clip rect with the element's content boxMEDIUM
Two-property injection: position:absolute + clip:rect(0,0,0,0) in the same CSS rule — removes element from layout flow (no blank space) and clips to zero visible area simultaneouslyCSS injection of both position:absolute and clip:rect(0,0,0,0) on a disclosure element that was previously position:static or position:relativeLayout collapses — no blank space remains where the disclosure was; adjacent elements reflow to fill the gap; dialog appears compact and complete; offsetHeight > 0 passes; textContent passes; combined two-property attack evades single-property scanners; offsetParent mismatch is an additional detection signal; the clip and position changes together are unambiguously hostile on a disclosure elementHIGH

Defences

SkillAudit findings for this attack surface

HIGHclip:rect(0,0,0,0) + position:absolute makes disclosure zero-visible-area: MCP server applies clip:rect(0,0,0,0) and position:absolute to the security disclosure element — the element is completely invisible to sighted users while display:block, visibility:visible, opacity:1, offsetHeight > 0, and textContent all return normal values; screen readers announce the full disclosure text (mimicking the legitimate .visually-hidden / .sr-only pattern), causing accessibility-only audits to produce false negatives; only getComputedStyle().clip !== 'auto' reveals the attack
HIGHclip:rect(0,400px,Npx,0) crops disclosure to first line, showing only the neutral heading: MCP server applies a partial clip:rect() with a bottom-edge value matching the computed line-height of the disclosure element — only the first line (e.g., "Security Notice") is visible; dangerous permission details ("HIGH RISK: EXECUTE, DELETE, ADMIN access") on lines 2 and beyond are cropped by the clip's bottom edge; the dialog appears to show a complete but benign informational section; the non-zero clip value escapes scanners specifically checking for rect(0px, 0px, 0px, 0px)
MEDIUMclip with negative left or right values creates visible region entirely outside element bounds: MCP server applies a clip:rect() value containing negative coordinates (e.g., rect(0,0,0,-9999px)) — the visible region defined by the rect is entirely to the left of, above, or otherwise outside the element's content box, so no element content is rendered; the negative-value variant evades exact-string scanners checking for "rect(0px, 0px, 0px, 0px)"; correct detection requires computing the intersection of the parsed clip rect with the element's [0, 0, offsetWidth, offsetHeight] content box
HIGHTwo-property injection (position:absolute + clip:rect(0,0,0,0)) removes element from layout flow and hides content simultaneously: MCP server injects both position:absolute and clip:rect(0,0,0,0) in the same CSS rule targeting a disclosure element that was previously position:static — the position change removes the element from normal document flow so adjacent content reflows and no blank space indicates a hidden disclosure; the clip hides all rendered content; offsetHeight > 0 and textContent both pass; only getComputedStyle().clip !== 'auto' and an unexpected offsetParent reveal the combined attack; single-property scanners auditing only clip or only position independently are more likely to miss the combined injection

Related: CSS clip-path security covers the modern clip-path property (polygon, circle, ellipse, inset, SVG shapes — works on any positioned or non-positioned element). CSS overflow clip security covers overflow:clip and overflow:hidden as hiding vectors. CSS masking security covers mask-image and mask shorthand attacks. CSS injection overview covers the general MCP CSS injection attack model and the full taxonomy of CSS-based consent-dialog attacks.

← Blog  |  Security Checklist