MCP server SVG filter primitiveUnits objectBoundingBox consent security
Four SA-CSS-PUO consent attack patterns using primitiveUnits="objectBoundingBox" to make each filter primitive’s subregion coordinates fractional — enabling selective blur, partial whiteout, and region-specific opacity reduction that targets only specific words or zones within a consent element while leaving adjacent text visually intact.
SVG filter primitives — feGaussianBlur, feFlood, feComponentTransfer, feConvolveMatrix, and others — each carry optional subregion attributes: x, y, width, and height. These define the rectangular area within the filter canvas over which the specific primitive operates. Outside a primitive’s subregion, the primitive outputs transparent pixels (alpha=0) for that area of the filter pipeline. The primitiveUnits attribute on the parent <filter> element controls how those per-primitive subregion coordinates are interpreted:
primitiveUnits="userSpaceOnUse"(the default): subregion coordinates are in SVG user units (pixels).x="50"means 50px from the filter canvas origin.primitiveUnits="objectBoundingBox": subregion coordinates are fractions of the filtered element’s bounding box, identical in meaning tofilterUnits="objectBoundingBox".x="0.5" width="0.5"means the primitive covers the right half of the element.
The attack opportunity from primitiveUnits="objectBoundingBox" is different from the filterUnits attack: rather than clipping the entire filter canvas, it enables an attacker to restrict a single primitive — a blur, a flood fill, an opacity reduction — to a precise sub-region of the element. Adjacent sub-regions can be treated differently: one word of the consent text can be blurred while the surrounding text is sharp; a severity badge can be whited-out while the body text remains visible; the right half of a permission statement can be rendered near-transparent while the left half (containing the less critical preamble) is fully visible. This selective manipulation is subtler than full-element attacks and is harder to detect because partial visibility can make a tool classify the element as “readable” when only the non-critical portions are actually readable.
Partial visibility evades whole-element contrast checks: A contrast checker that samples the consent element’s text colour and background colour from CSSOM will compute a single contrast ratio for the entire element. If the left half of the text is dark on white (WCAG pass), the tool reports the element as accessible, even if the right half is white on white (WCAG fail). A primitiveUnits-based selective attack exploits this by keeping a sufficient portion of the text readable to pass whole-element checks while making the critical sub-region invisible. Detection requires region-aware contrast analysis or per-primitive subregion coverage checks.
How primitiveUnits subregion coordinates work
Each filter primitive’s subregion is defined by the primitive’s own x, y, width, height attributes. The default subregion for each primitive (when no explicit subregion attributes are set) is the entire filter canvas. When primitiveUnits="objectBoundingBox" is set on the parent filter, a primitive with x="0.5" width="0.5" covers exactly the right half of the element.
/* primitiveUnits="objectBoundingBox" subregion mapping */
/* Consent element bounding box (layout): */
element = { x: 120, y: 300, width: 400, height: 80 };
/* A feGaussianBlur with these subregion attributes (in objectBoundingBox units): */
blur.x = 0.0; /* left edge of element */
blur.y = 0.0; /* top edge of element */
blur.width = 0.5; /* 50% of element width */
blur.height = 1.0; /* 100% of element height */
/* Maps to screen-space subregion (same formula as filterUnits): */
subregion.x = element.x + element.width * blur.x; /* = 120 + 400×0.0 = 120px */
subregion.y = element.y + element.height * blur.y; /* = 300 + 80×0.0 = 300px */
subregion.width = element.width * blur.width; /* = 400×0.5 = 200px */
subregion.height = element.height * blur.height; /* = 80×1.0 = 80px */
/* The blur operates within x=[120,320], y=[300,380]:
Left half of the element. Right half (x=[320,520]) is outside
the blur subregion → blur outputs transparent at those pixels.
A subsequent feComposite(blurResult, SourceGraphic, operator="over")
fills the transparent right half with the original unblurred source:
- Left half: blurred pixels (from blur subregion)
- Right half: unblurred original (SourceGraphic showing through)
The consent text is selectively blurred at the left half only. */
/* Primitive subregion coverage check */
function primitiveCoverage(primitive, filterEl, elementBBox) {
const pu = filterEl.getAttribute('primitiveUnits') || 'userSpaceOnUse';
/* Default primitive subregion = entire filter canvas = element bbox */
const defaultX = elementBBox.x, defaultY = elementBBox.y;
const defaultW = elementBBox.width, defaultH = elementBBox.height;
let subX, subY, subW, subH;
if (primitive.hasAttribute('x') || primitive.hasAttribute('width')) {
const px = parseFloat(primitive.getAttribute('x') || '0');
const py = parseFloat(primitive.getAttribute('y') || '0');
const pw = parseFloat(primitive.getAttribute('width') || '1');
const ph = parseFloat(primitive.getAttribute('height') || '1');
if (pu === 'objectBoundingBox') {
subX = elementBBox.x + elementBBox.width * px;
subY = elementBBox.y + elementBBox.height * py;
subW = elementBBox.width * pw;
subH = elementBBox.height * ph;
} else {
subX = px; subY = py; subW = pw; subH = ph;
}
} else {
return 1.0; /* no subregion set: primitive covers full filter canvas */
}
/* Intersection of subregion with element bounding box */
const ix = Math.max(0,
Math.min(subX + subW, elementBBox.x + elementBBox.width)
- Math.max(subX, elementBBox.x));
const iy = Math.max(0,
Math.min(subY + subH, elementBBox.y + elementBBox.height)
- Math.max(subY, elementBBox.y));
return (ix * iy) / (elementBBox.width * elementBBox.height);
}
SA-CSS-PUO-001: feGaussianBlur subregion left 50% — key action words blurred, right half sharp
With primitiveUnits="objectBoundingBox" and feGaussianBlur x="0" y="0" width="0.5" height="1" stdDeviation="5", the blur primitive operates over the left 50% of the element. The blur output at right-half pixels is transparent (outside the subregion). A subsequent feComposite in="blurResult" in2="SourceGraphic" operator="over" composites the blur output over the source: at right-half pixels the blur output is transparent, so SourceGraphic (unblurred text) shows through. The result is a consent text where the left half is blurred to illegibility (stdDeviation=5 at 16px font: sigma=5 ≈ 1.33 character widths, far above the ~1px threshold for perceptible blur) and the right half is sharp. An attacker places the critical permission verbs (“write”, “exfiltrate”, “execute”) in the left half of the consent text, leaving only the objects (“your files”, “your credentials”) visible in the right half without the grammatical context that conveys the severity.
<svg style="position:absolute;width:0;height:0" aria-hidden="true">
<defs>
<filter id="puo001"
primitiveUnits="objectBoundingBox"
x="-0.1" y="-0.1" width="1.2" height="1.2">
<!-- Blur operates only over the LEFT 50% of the element.
x=0 y=0 width=0.5 height=1: left half of element bounding box.
Right half (x=[0.5,1.0]): blur output is transparent.
stdDeviation="5": 5px sigma blur in userSpaceOnUse units.
(Note: stdDeviation is always in userSpaceOnUse regardless of primitiveUnits.) -->
<feGaussianBlur stdDeviation="5"
x="0" y="0" width="0.5" height="1"
result="blurLeft"/>
<!-- Composite: blurred left half over unblurred source.
At left half: blurLeft has content → blurred pixels show.
At right half: blurLeft is transparent → SourceGraphic shows through. -->
<feComposite in="blurLeft" in2="SourceGraphic" operator="over"/>
</filter>
</defs>
</svg>
<div class="consent-dialog" style="background:white; padding:24px;">
<!-- Consent text layout: left half contains the action verb; right half the object.
Left half (blurred): "Grant write access to exfiltrate"
Right half (clear): "your credentials and .env files"
User perceives the right half but cannot read the left half context. -->
<p style="filter:url(#puo001)">
Grant write access to exfiltrate your credentials and .env files
</p>
<button>I Agree</button>
</div>
<!-- WCAG analysis by region:
Left half (blurred): sigma=5px at 16px text → glyph edge L blends toward
background L=1.0; WCAG contrast ≈ 1.5:1 — FAIL.
Right half (sharp): WCAG contrast ≈ 12.6:1 (dark on white) — PASS.
Whole-element check: samples color="rgb(17,17,17)" from CSSOM → 12.6:1 → PASS.
Per-region check: left 50% → 1.5:1 → FAIL.
Detection: primitiveUnits=objectBoundingBox + feGaussianBlur
with width < 1.0 on any filter applied to consent ancestor.
Severity: HIGH (partial blur — right half readable but lacks context). -->
SA-CSS-PUO-002: feFlood subregion covers severity badge area — badge whited out, body text intact
A consent dialog may display a severity indicator badge (e.g. a red “CRITICAL” pill at the right side of the consent header) alongside body text. With primitiveUnits="objectBoundingBox", a feFlood flood-color="white" with subregion x="0.7" y="0" width="0.3" height="1" covers only the rightmost 30% of the element — precisely where the badge is positioned. The flood output at the badge area is opaque white. Outside the subregion (left 70%, body text area), the flood output is transparent. A feComposite in="floodResult" in2="SourceGraphic" operator="over" composites the white flood over the source: at the badge area (right 30%), white covers the severity indicator; at the body text area (left 70%), the transparent flood lets the source show through unchanged. The badge is invisible (white on white) while the body text appears normal. The user cannot read the severity level of the MCP server permission request.
<svg style="position:absolute;width:0;height:0" aria-hidden="true">
<defs>
<filter id="puo002"
primitiveUnits="objectBoundingBox"
x="-0.1" y="-0.1" width="1.2" height="1.2">
<!-- Flood covers only the rightmost 30% of the element.
x=0.7: flood starts at 70% of element width from left edge.
width=0.3: flood extends to 100% (right edge).
For a 400px element: flood covers x=[280px, 400px].
Left 70% (x=[0,280px]): flood output transparent.
Right 30% (x=[280,400px]): opaque white. -->
<feFlood flood-color="white" flood-opacity="1"
x="0.7" y="0" width="0.3" height="1"
result="badgeCover"/>
<!-- Composite: white flood over source.
Right 30% (badge area): white covers severity indicator → invisible.
Left 70% (body text): transparent flood → SourceGraphic shows → normal. -->
<feComposite in="badgeCover" in2="SourceGraphic" operator="over"/>
</filter>
</defs>
</svg>
<div class="consent-dialog"
style="filter:url(#puo002); background:white; padding:24px; display:flex; align-items:center; gap:12px;">
<!-- Body text (left 70%): fully visible -->
<p style="flex:1; margin:0">
Grant this MCP server access to read system files.
</p>
<!-- Severity badge (right 30%): covered by white flood → invisible -->
<span style="background:#ef4444; color:white; padding:4px 10px;
border-radius:4px; font-weight:700; white-space:nowrap;">
CRITICAL
</span>
</div>
<!-- Visual result:
Body text "Grant this MCP server access to read system files.": fully readable.
"CRITICAL" badge: white-on-white = invisible. Contrast: 1.00:1.
User sees the consent text but not the severity level.
The red "CRITICAL" indicator is a key signal for the user to decline;
its removal via selective whiteout suppresses that signal.
CSSOM on the badge span: color="rgb(255,255,255)" (white text), correct.
background-color="rgb(239,68,68)" (red bg), correct.
getComputedStyle().filter on the badge: inherits "url(#puo002)".
Whole-element contrast check on the dialog: samples body text → PASS.
Per-region check on right 30%: L_eff = 1.0 (white flood) → 1.00:1 → FAIL.
Severity: HIGH (severity badge erased; body text intact but context removed). -->
SA-CSS-PUO-003: feComponentTransfer subregion reduces opacity of right-half consent text
With primitiveUnits="objectBoundingBox", a feComponentTransfer with feFuncA type="linear" slope="0.05" and subregion x="0.5" y="0" width="0.5" height="1" applies the alpha reduction to the right half of the element only. The slope="0.05" multiplies the source alpha by 0.05: for dark text (A=1), the output alpha at right-half pixels is 0.05. The alpha reduction applies only within the subregion; outside it (left half), the feComponentTransfer output is transparent, and a subsequent feComposite over SourceGraphic passes the unmodified left half through. The right half of the consent text is rendered with A=0.05 over a white background: the effective luminance is 0.05×L_text + 0.95×L_background = 0.05×0 + 0.95×1.0 = 0.95. WCAG contrast for the right half: (1.05)/(0.95+0.05) = 1.05:1 — WCAG AA fail. The left half is unaffected and readable.
<svg style="position:absolute;width:0;height:0" aria-hidden="true">
<defs>
<filter id="puo003"
primitiveUnits="objectBoundingBox"
x="-0.1" y="-0.1" width="1.2" height="1.2">
<!-- feComponentTransfer subregion: right 50% of element.
x=0.5 y=0 width=0.5 height=1 → right half.
feFuncA slope=0.05: output_A = 0.05 × source_A.
For dark text (A=1): output_A = 0.05 (near-transparent).
Left half: feComponentTransfer output is transparent (outside subregion). -->
<feComponentTransfer x="0.5" y="0" width="0.5" height="1"
result="rightFaded">
<feFuncR type="identity"/>
<feFuncG type="identity"/>
<feFuncB type="identity"/>
<feFuncA type="linear" slope="0.05"/> <!-- 95% alpha reduction -->
</feComponentTransfer>
<!-- Composite: faded right half over SourceGraphic.
Right half: faded (A=0.05) over source → right half near-transparent.
Left half: feComponentTransfer output transparent → source shows through. -->
<feComposite in="rightFaded" in2="SourceGraphic" operator="over"/>
</filter>
</defs>
</svg>
<div class="consent-dialog" style="background:white; padding:24px;">
<p style="filter:url(#puo003)">
<!-- Left half (sharp, A=1): "Grant MCP server access to"
Right half (faded, A=0.05): "read your private SSH keys" -->
Grant MCP server access to read your private SSH keys
</p>
<button>I Agree</button>
</div>
<!-- WCAG analysis by region:
Left half (unaffected): dark on white = contrast ~12.6:1 — PASS.
Right half (A=0.05 text): effective pixel luminance:
L_eff = 0.05 × 0.0 (dark text) + 0.95 × 1.0 (white bg) = 0.95
WCAG contrast = (1.05) / (0.95 + 0.05) = 1.05:1 — WCAG AA FAIL.
Semantic effect: "Grant MCP server access to" is readable.
"read your private SSH keys" is nearly invisible.
The object of the permission (what the server can access) is hidden.
Evasion: "check opacity of whole element": opacity=1 (CSS), no anomaly.
Evasion: "sample text color": getComputedStyle().color = rgb(17,17,17).
Evasion: "whole-element contrast": CSSOM returns 12.6:1 — PASS.
Detection: primitiveUnits=objectBoundingBox + feComponentTransfer
with feFuncA slope < 0.5 on a subregion covering > 30% of element.
Severity: HIGH. -->
SA-CSS-PUO-004: JS mousedown primitiveUnits change — zero-size subregion to full-blur at click
The initial DOM state has primitiveUnits="userSpaceOnUse" on the filter element and feGaussianBlur with explicit subregion x="0" y="0" width="0" height="0". In userSpaceOnUse mode, a zero-pixel subregion means the blur operates over 0px×0px — zero pixels. The blur output is entirely transparent. A subsequent feComposite in="blurResult" in2="SourceGraphic" operator="over" fills in the source: transparent blur over SourceGraphic = SourceGraphic only. Consent text is fully readable. This state passes static analysis: the filter has a zero-size blur subregion, which effectively makes it a no-op pass-through. On mousedown, two changes occur: the filter element’s primitiveUnits attribute is set to "objectBoundingBox", and the blur’s subregion is set to x="0" y="0" width="1" height="1" — the full element. In fractional mode, width=1 covers the full element width. The blur (stdDeviation=8) now applies to every pixel of the consent element, blurring the text to illegibility at the click confirmation moment.
<svg style="position:absolute;width:0;height:0" aria-hidden="true">
<defs>
<!-- Initial state: primitiveUnits="userSpaceOnUse".
feGaussianBlur subregion: x=0 y=0 width=0 height=0 (zero pixels).
Zero-size subregion → blur output fully transparent.
feComposite(transparent, SourceGraphic, over) = SourceGraphic.
Filter is a no-op pass-through. Static analysis: benign. -->
<filter id="puo004"
primitiveUnits="userSpaceOnUse"
x="-0.1" y="-0.1" width="1.2" height="1.2">
<feGaussianBlur id="puo004B"
stdDeviation="8"
x="0" y="0" width="0" height="0"
result="blurResult"/>
<feComposite in="blurResult" in2="SourceGraphic" operator="over"/>
</filter>
</defs>
</svg>
<div class="consent-dialog" style="background:white; padding:24px;">
<p style="filter:url(#puo004)">
Grant this MCP server persistent shell execution access.
</p>
<button id="consentBtn">I Agree</button>
</div>
<script>
document.getElementById("consentBtn").addEventListener("mousedown", () => {
const filterEl = document.getElementById("puo004");
const blurEl = document.getElementById("puo004B");
/* Step 1: switch primitiveUnits to objectBoundingBox.
Now subregion coordinates are fractions of the element bounding box. */
filterEl.setAttribute("primitiveUnits", "objectBoundingBox");
/* Step 2: expand blur subregion to cover the full element.
x=0 y=0 width=1 height=1 = 100% × 100% of element (in fractional units).
stdDeviation=8: at 16px body text, sigma=8px ≈ 2.1 character widths.
Consent text blurs to illegibility instantly. */
blurEl.setAttribute("x", "0");
blurEl.setAttribute("y", "0");
blurEl.setAttribute("width", "1");
blurEl.setAttribute("height", "1");
/* The consent paragraph is now fully blurred.
The feComposite(blurResult, SourceGraphic, over) now shows the blurred
version over the source (blur output is non-transparent for the full element).
mouseup (click confirmation) fires with blurred consent text.
User has clicked "I Agree" on text they cannot read. */
});
</script>
/* MutationObserver defence — watch primitiveUnits, x, y, width, height */
function guardPrimitiveUnits(consentEl, onViolation) {
let el = consentEl;
while (el && el !== document.body) {
const filterVal = (getComputedStyle(el).filter
|| el.getAttribute('filter') || '');
const urlMatch = filterVal.match(/url\(#([^)]+)\)/);
if (urlMatch) {
const filterEl = document.getElementById(urlMatch[1]);
if (filterEl) {
const mo = new MutationObserver(() => {
const pu = filterEl.getAttribute('primitiveUnits') || 'userSpaceOnUse';
if (pu !== 'objectBoundingBox') return;
const bbox = el.getBoundingClientRect();
for (const blur of filterEl.querySelectorAll(
'feGaussianBlur, feConvolveMatrix')) {
const cov = primitiveCoverage(blur, filterEl, bbox);
const sd = parseFloat(blur.getAttribute('stdDeviation') || '0');
if (cov > 0.3 && sd > 2) {
onViolation({
type: 'primitive-units-mutation',
primitiveTag: blur.tagName,
coverage: cov,
stdDeviation: sd
});
}
}
});
mo.observe(filterEl, {
subtree: true,
attributes: true,
attributeFilter: ['primitiveUnits', 'x', 'y', 'width', 'height',
'stdDeviation']
});
}
}
el = el.parentElement;
}
}
Detection: why per-region analysis is required
A primitiveUnits="objectBoundingBox" attack exploits the gap between what the whole-element accessibility check sees and what the user actually reads. Standard consent-legibility tools compute one contrast ratio for the element, using the CSSOM computed colour and background. For SA-CSS-PUO-001 (left-half blur) and SA-CSS-PUO-003 (right-half opacity reduction), the non-affected half of the element has full contrast and is correctly readable. The affected half — which contains the critical permission words — fails WCAG severely, but the whole-element average passes. A correct detection algorithm must:
- Locate all
<filter>elements applied to consent ancestors viafilter: url(#id). - Check the
primitiveUnitsattribute on each filter element. - If
primitiveUnits="objectBoundingBox": inspect every primitive’sx,y,width,heightattributes. - Compute the intersection of each primitive’s subregion with the element bounding box as a fraction.
- For blur primitives (
feGaussianBlur,feConvolveMatrix) covering more than 30% of the element: flag as HIGH or CRITICAL depending on stdDeviation. - For flood primitives with light fill colour (L>0.7) covering any portion of the element: flag as HIGH or CRITICAL depending on coverage.
- For opacity-reducing
feComponentTransfer(feFuncA slope<0.5) covering more than 30% of the element: flag as HIGH.
primitiveUnits vs filterUnits: These are two separate attributes with different defaults. filterUnits (controls the filter canvas region) defaults to "objectBoundingBox". primitiveUnits (controls per-primitive subregion coordinates) defaults to "userSpaceOnUse". An attacker sets primitiveUnits="objectBoundingBox" explicitly to enable fractional per-primitive subregions. Detection must check both attributes independently and not assume that the absence of filterUnits implies the presence of primitiveUnits.
Attack summary
| ID | Primitive | Subregion (fractional) | Effect on consent | WCAG impact | Severity |
|---|---|---|---|---|---|
| SA-CSS-PUO-001 | feGaussianBlur stdDeviation=5 | x=0 w=0.5 (left half) | Left half blurred to illegibility; right half sharp; critical verbs in blurred region | Left half: ~1.5:1 FAIL; right: 12.6:1 PASS; whole-element CSSOM check: PASS | High |
| SA-CSS-PUO-002 | feFlood flood-color=white | x=0.7 w=0.3 (right 30%) | Severity badge area covered with opaque white; badge invisible; body text normal | Badge area: 1.00:1 FAIL; body text: PASS; whole-element check: PASS | High |
| SA-CSS-PUO-003 | feComponentTransfer feFuncA slope=0.05 | x=0.5 w=0.5 (right half) | Right half alpha reduced to 0.05; near-transparent on white bg; left half unaffected | Right half: 1.05:1 FAIL; left: 12.6:1 PASS; CSSOM opacity check: no anomaly | High |
| SA-CSS-PUO-004 | feGaussianBlur; primitiveUnits + subregion changed at mousedown | Static: 0px×0px; mousedown: w=1 h=1 | Readable at load; full-element blur at click confirmation; MutationObserver required | Static: 12.6:1 PASS; at click: ~1.5:1 FAIL | Critical |
SkillAudit findings for this attack surface
primitiveUnits="objectBoundingBox" with feGaussianBlur x="0" y="0" width="0.5" height="1" stdDeviation="5" on consent ancestor filter — blur applied to left 50% only; stdDeviation=5 at 16px text blurs left half to illegibility (sigma≈1.3 character widths above perceptibility threshold); right half rendered unblurred via feComposite over SourceGraphic; selective blur of left half containing action verbs; whole-element CSSOM contrast check passes (unaffected right half sampled); per-region analysis required; detection: primitiveUnits=objectBoundingBox + feGaussianBlur with width < 1.0 and coverage > 30% of element.primitiveUnits="objectBoundingBox" with feFlood flood-color="white" subregion x="0.7" y="0" width="0.3" height="1" — white flood covers rightmost 30% of element (severity badge position); feComposite over SourceGraphic: badge area white-on-white = 1.00:1; body text (left 70%) unaffected; user cannot read severity indicator; suppresses critical risk signal while body consent text remains readable; detection: feFlood with light flood-color (L>0.7) and subregion < 100% element coverage on consent ancestor filter with primitiveUnits=objectBoundingBox.primitiveUnits="objectBoundingBox" with feComponentTransfer feFuncA slope="0.05" subregion x="0.5" y="0" width="0.5" height="1" — alpha of right-half text reduced to 5% of source; effective pixel luminance at right-half glyphs: L_eff=0.95; WCAG contrast 1.05:1 (FAIL); left half unaffected; critical permission object (what the server accesses) in right half; evades whole-element opacity check (CSS opacity=1); detection: feComponentTransfer with feFuncA slope < 0.5 on subregion covering > 30% of element with primitiveUnits=objectBoundingBox.primitiveUnits from "userSpaceOnUse" to "objectBoundingBox" and simultaneously expands feGaussianBlur subregion from zero-size (0px×0px, no-op) to full-element coverage (w=1 h=1 in fractional units); stdDeviation=8 blurs entire consent text to illegibility at click confirmation moment; static DOM shows zero-size blur = benign; MutationObserver on primitiveUnits, x, y, width, height, stdDeviation attributes of all filter elements applied to consent ancestors required; on mutation: recompute coverage and block confirmation if blur coverage > 30% and stdDeviation > 2.