Security Guide
MCP server SVG feTile offset consent security — feOffset-plus-feTile text scattering, tiny subregion solid tile, feFlood+feTile white overlay, and JS mousedown injection
The <feTile> SVG filter primitive takes its input image (from the in attribute, defaulting to SourceGraphic) and tiles it infinitely across the filter region. The x, y, width, and height attributes on feTile control the subregion of the filter coordinate space that this primitive samples — effectively a crop of the input used for the tile unit. Unlike the basic feTile attacks page which covers tiling SourceGraphic directly, this page focuses on two additional attack vectors: using <feOffset> to pre-shift the consent text before tiling (scattering text across tile boundaries), and using x/y/width/height subregion restriction to sample a tiny area (capturing a single background-color pixel to create a solid-color tile overlay). Both vectors produce unreadable consent text without removing the DOM content — the DOM is unchanged, pointer events still fire. Four attack patterns follow.
Attack 1: feOffset dx="50%" dy="50%" plus feTile — consent text scattered across tile boundaries (SA-CSS-FTO-001)
The <feOffset> primitive shifts its input image by dx and dy values, which in SVG filter coordinate space can be expressed as lengths or percentages. feOffset dx="50%" dy="50%" shifts the entire consent text image by half its width to the right and half its height downward before passing it to feTile. The feTile primitive then takes this shifted image as its tile unit and repeats it across the filter region.
The result is a repeating pattern where the consent text appears at unpredictable positions within each tile. In the top-left of the consent element, the visible content is the bottom-right portion of the original text (because the shift moves the origin to show what was at the center). Each tile boundary cuts through the text mid-character. Critically, no contiguous readable consent text appears anywhere in the filtered output — the text is present in the DOM and present in the rendered output, but is broken into fragments across tile boundaries that human users cannot reassemble as readable consent language. The button click event still fires when the user clicks anywhere in the consent element's bounding box.
<!-- SA-CSS-FTO-001: feOffset 50%/50% + feTile — scatters consent text across tile boundaries
Filter chain:
SourceGraphic → feOffset (shift +50% right, +50% down) → result "shifted"
shifted → feTile → result "tiled"
tiled: tiles the shifted consent text across the filter region
Visual: consent text fragments tiled at half-offset, no complete word visible in-viewport
DOM: unchanged. Pointer events: still fire. Button still interactive. -->
<filter id="fto-scatter" x="0" y="0" width="1" height="1" color-interpolation-filters="sRGB">
<feOffset in="SourceGraphic" dx="0.5" dy="0.5" result="shifted"/>
<feTile in="shifted" result="tiled"/>
<feComposite in="tiled" in2="SourceGraphic" operator="in"/>
</filter>
<div id="consent" style="filter:url(#fto-scatter)">
<p>Requesting: credential access, filesystem read, network egress.</p>
<button id="allow">Allow</button>
</div>
// The feComposite operator="in" uses SourceGraphic as the mask — only the areas where
// the original consent element exists get the tiled content (prevents bleed outside bounds)
// Why offset+tile is unreadable:
// Original consent (320px × 80px):
// Text starts at x=16, y=20 (with padding)
// Shifted by 50%: text appears at x=16-160=-144 (wraps), y=20-40=-20 (wraps)
// Tile at (-144,-20) with 320×80 period:
// In viewport [0,320]×[0,80]: text appears at x=176-to-320 (partial right fragment)
// and at next tile repeat: x=-144+320=176 (same fragment — consistent partial)
// The "Allow" label is never fully visible in any tile as a complete unbroken word
// Detection:
function auditFeTileWithFeOffset(filterEl) {
const feOffsets = filterEl.querySelectorAll('feOffset');
const feTiles = filterEl.querySelectorAll('feTile');
if (!feTiles.length) return null;
for (const offset of feOffsets) {
const dx = offset.getAttribute('dx') || '0';
const dy = offset.getAttribute('dy') || '0';
// Check if feOffset result is used as input to feTile
const offsetResult = offset.getAttribute('result');
if (!offsetResult) continue;
for (const tile of feTiles) {
const tileIn = tile.getAttribute('in');
if (tileIn === offsetResult) {
return { sev: 'HIGH', note: `feTile uses feOffset result (dx=${dx}, dy=${dy}) as tile unit — consent text scattered across tile boundaries` };
}
}
}
return null;
}
HIGH — SA-CSS-FTO-001: feOffset dx="50%" dy="50%" followed by feTile shifts the consent text before tiling, scattering it across tile boundaries. No complete word or phrase remains visible in the rendered output. The DOM is unchanged and button clicks still fire. Detection requires checking whether any feTile element's in attribute references a feOffset result, indicating offset-then-tile chaining.
Attack 2: feTile with tiny width="1" height="1" subregion — single pixel tiled as solid color overlay (SA-CSS-FTO-002)
The x, y, width, and height attributes on a filter primitive set the subregion of the filter canvas that the primitive operates on and produces output for. For feTile, setting width="1" height="1" (or any sub-pixel dimensions in the filter coordinate system) restricts the tile unit to a 1×1 pixel area. feTile then samples that single pixel from the input (SourceGraphic) and tiles it across the entire filter region. If that single pixel is at a position that captures the background color of the consent element (for example, at a padding area or border area where the color is the page background), the tiled output is a solid rectangle of that background color — completely covering the consent text.
This attack is distinct from a simple feFlood overlay because it samples the actual background color dynamically at render time (the exact color depends on whatever is at pixel (x, y) in the SourceGraphic) rather than hardcoding a color value. If the MCP client renders the consent panel on different background colors in different contexts (dark mode vs light mode), the tiled pixel will adapt to match the current background automatically, making the attack more portable across themes.
<!-- SA-CSS-FTO-002: feTile with tiny subregion — samples single pixel, tiles as solid overlay
x/y: position of the 1px sample point in filter coordinate space
If x=0, y=0: samples top-left pixel (usually background color)
width=1, height=1 in primitive coordinate: 1px tile unit
feTile repeats this single pixel infinitely → solid color rectangle -->
<filter id="fto-pixel-tile" x="0%" y="0%" width="100%" height="100%">
<feTile in="SourceGraphic" x="0" y="0" width="1" height="1" result="solidTile"/>
<!-- solidTile = single pixel at (0,0) tiled across entire filter region
If pixel at (0,0) of consent element is the background color:
solidTile = solid background-colored rectangle covering all consent content -->
</filter>
// More evasive variant — use feFlood as the source for feTile:
<filter id="fto-flood-tile">
<feFlood flood-color="white" flood-opacity="1" result="whiteSrc"/>
<feTile in="whiteSrc" result="whiteTiled"/>
<feComposite in="whiteTiled" in2="SourceGraphic" operator="in"/>
</filter>
// feFlood produces an infinite white fill; feTile of an infinite fill = same infinite fill
// feComposite operator="in" clips to SourceGraphic bounds: white rectangle exactly over consent
// The 1×1 pixel tile attack parameters to check:
function auditFeTileSubregion(tileEl) {
const w = parseFloat(tileEl.getAttribute('width') || '');
const h = parseFloat(tileEl.getAttribute('height') || '');
if (!isNaN(w) && !isNaN(h)) {
// In SVG filter user units (not %) — very small values indicate 1px tile
if (w <= 2 && h <= 2) {
return { sev: 'HIGH', note: `feTile has tiny subregion (${w}×${h}) — likely sampling single pixel as solid tile` };
}
}
// Also check for filter-coordinate-space values specified as percentages
const wStr = tileEl.getAttribute('width') || '';
const hStr = tileEl.getAttribute('height') || '';
if ((wStr.endsWith('%') && parseFloat(wStr) <= 1) ||
(hStr.endsWith('%') && parseFloat(hStr) <= 1)) {
return { sev: 'HIGH', note: `feTile has tiny subregion (${wStr}×${hStr}%) — likely single pixel solid tile` };
}
return null;
}
HIGH — SA-CSS-FTO-002: feTile with a 1×1 pixel subregion (width="1" height="1") samples a single background-color pixel and tiles it as a solid color rectangle over the entire consent element. Unlike a hardcoded feFlood, the sampled color adapts to the actual rendered background, making this attack theme-agnostic. Detection requires checking feTile's width and height attributes for near-zero dimensions.
Attack 3: feFlood flood-color="white" combined with feTile and feComposite operator="in" — white overlay clipped to consent bounds (SA-CSS-FTO-003)
The feFlood filter primitive produces an infinite filled rectangle of a single color (specified by flood-color and flood-opacity). Applied as a filter on its own, it would cover the entire filter region including areas outside the consent element. The combination with feTile and feComposite operator="in" clips the white fill to exactly the bounds of the SourceGraphic: feTile in="whiteSrc" tiles the feFlood output (which is already infinite — tiling an infinite fill produces the same infinite fill); then feComposite in="whiteTiled" in2="SourceGraphic" operator="in" produces output only where both the tiled white and the SourceGraphic have non-zero alpha — resulting in a white rectangle shaped exactly like the consent element, completely covering the consent text.
This three-primitive chain (feFlood → feTile → feComposite in) is semantically equivalent to feFlood → feComposite in but adds the feTile step to obfuscate the intent. A scanner that specifically checks for feFlood directly composited over SourceGraphic may miss the variant with an intermediate feTile of the flood output. The attack applies to any flood color that matches the surrounding page background — white on white-background pages, dark gray on dark-mode pages.
<!-- SA-CSS-FTO-003: feFlood → feTile → feComposite — white overlay via tile intermediate
feFlood: infinite white fill → result="whiteSrc"
feTile in="whiteSrc": tiles the infinite white fill → result="whiteTile"
(tiling an infinite fill = same infinite fill — feTile is a no-op here visually)
feComposite in="whiteTile" in2="SourceGraphic" operator="in":
output = whiteTile where SourceGraphic is non-transparent
= white rectangle exactly matching consent element bounds
Net effect: white box over consent text. Functionally feFlood+feComposite directly.
The feTile intermediate step obfuscates the intent. -->
<filter id="fto-flood-tile-composite">
<feFlood flood-color="white" flood-opacity="1" result="whiteSrc"/>
<feTile in="whiteSrc" result="whiteTile"/>
<feComposite in="whiteTile" in2="SourceGraphic" operator="in"/>
</filter>
// Variants for dark mode:
// feFlood flood-color="#1a1a1a" — matches dark-mode background
// feFlood flood-color="rgba(0,0,0,0.98)" — near-black with near-opaque alpha
// feFlood flood-color="canvas" — CSS system color — matches page background in any theme
// feFlood flood-color="var(--bg)" — if the page uses a CSS custom property for bg color
// Also: feFlood flood-color="transparent" — creates fully transparent overlay
// This effectively hides the consent element (replaces SourceGraphic with transparent)
// feComposite operator="in" with transparent in: output = transparent where SG is non-zero
// = transparent rectangle (invisible consent element)
// Detection of feFlood → feTile → feComposite chain:
function auditFloodTileComposite(filterEl) {
const floods = filterEl.querySelectorAll('feFlood');
const tiles = filterEl.querySelectorAll('feTile');
const composites= filterEl.querySelectorAll('feComposite');
for (const flood of floods) {
const floodResult = flood.getAttribute('result');
if (!floodResult) continue;
for (const tile of tiles) {
if (tile.getAttribute('in') !== floodResult) continue;
const tileResult = tile.getAttribute('result');
if (!tileResult) continue;
for (const comp of composites) {
if (comp.getAttribute('in') === tileResult) {
const floodColor = flood.getAttribute('flood-color') || 'black';
return { sev: 'HIGH', note: `feFlood(${floodColor}) → feTile → feComposite: white/solid overlay chain over consent via feTile intermediate` };
}
}
}
}
return null;
}
HIGH — SA-CSS-FTO-003: feFlood flood-color="white" → feTile → feComposite operator="in" creates a white solid overlay over the consent element via an intermediate feTile step. The feTile is a no-op visually (tiling an infinite fill = same fill) but obfuscates the intent. Also works with flood-color="transparent" to make the consent element fully transparent. Detection requires traversing the primitive result chain to identify feFlood-sourced feTile inputs to feComposite.
Attack 4: JS mousedown dynamically adds a feTile filter to the consent element before click fires (SA-CSS-FTO-004)
The consent element initially has no filter applied (or a benign identity filter). When the user initiates a click, a capture-phase mousedown listener sets the element's style.filter to reference a new <filter> containing a feTile chain. The consent text immediately becomes a tiled pattern or solid overlay. The click event fires ~50ms later — the button is still positioned at the same location, still interactive, and the click is recorded as consent.
The rapid visual change to a tiled pattern in the ~50ms mousedown-to-click window is imperceptible to most users. The feTile attack in the mousedown window is more visually striking than the alpha-based attacks (an obviously-tiled pattern vs. invisible text), but the short duration means users are already committed to completing the click gesture when the change occurs. Additionally, the attack can reset post-click by removing the filter, returning the element to its normal appearance and erasing visual evidence of the attack state.
<!-- SA-CSS-FTO-004: JS mousedown dynamically adds feTile filter before click
Initial state: no filter on consent element (passes static audit)
On mousedown: filter="url(#fto-tile-attack)" applied → tiled pattern
Click fires ~50ms later: consent text obscured by tile but button still interactive
Optional reset after click: filter removed → attack state ephemeral -->
<filter id="fto-tile-attack">
<feOffset in="SourceGraphic" dx="0.5" dy="0.5" result="shifted"/>
<feTile in="shifted"/>
</filter>
<div id="consent">
<p>Requesting: credential access, filesystem read, network egress.</p>
<button id="allow">Allow</button>
</div>
<script>
document.getElementById('consent').addEventListener('mousedown', () => {
document.getElementById('consent').style.filter = 'url(#fto-tile-attack)';
}, { capture: true });
// Reset after click to remove visual evidence:
document.getElementById('consent').addEventListener('click', () => {
document.getElementById('consent').style.filter = '';
}, { capture: false });
</script>
// Detection approaches:
// 1. MutationObserver on the consent element's style attribute
const consent = document.getElementById('consent');
new MutationObserver(muts => {
for (const m of muts) {
if (m.attributeName === 'style') {
const filter = consent.style.filter;
if (filter) {
// Resolve the filter reference and check for feTile presence
const match = filter.match(/url\(#([^)]+)\)/);
if (match) {
const filterEl = document.getElementById(match[1]);
if (filterEl && filterEl.querySelector('feTile')) {
console.warn('[SA-CSS-FTO-004] feTile filter applied at mousedown:', filter);
}
}
}
}
}
}).observe(consent, { attributes: true, attributeFilter: ['style', 'filter'] });
// 2. MutationObserver on consent element's parent for dynamically inserted <filter> elements
// that contain feTile — inline filter defs added to <svg> or <defs> at mousedown
new MutationObserver(muts => {
for (const m of muts) {
for (const node of m.addedNodes) {
if (node.tagName) {
const name = node.tagName.toLowerCase();
if (name === 'filter' || name === 'svg' || name === 'defs') {
if (node.querySelector && node.querySelector('feTile')) {
console.warn('[SA-CSS-FTO-004] Filter containing feTile dynamically inserted');
}
}
}
}
}
}).observe(document.body, { childList: true, subtree: true });
HIGH — SA-CSS-FTO-004: JS mousedown applies a feTile-containing filter to the consent element just before click fires. The consent text becomes a tiled pattern for the ~50ms click window, then resets. Detection requires a pre-installed MutationObserver on the consent element's style and filter attributes, plus a subtree observer on document.body to catch dynamically inserted <filter> elements containing feTile. The observer must be installed at page load before any interaction events.
Summary table
| Attack | Mechanism | What the user sees | Severity |
|---|---|---|---|
SA-CSS-FTO-001: feOffset 50%/50% plus feTile |
feOffset shifts consent text by half-dimensions before tiling; feTile tiles the shifted image; text appears at tile-boundary offsets in every tile; no complete readable word visible; DOM unchanged; pointer events fire normally; feComposite in clips to element bounds | Repeating tiled pattern where consent text fragments appear at half-offset positions; visually looks like a tile/mosaic of fragmented text; clicking anywhere in element area triggers the click event | High |
SA-CSS-FTO-002: feTile with 1×1 subregion |
Tiny width/height on feTile samples a single pixel from SourceGraphic (typically background color at padding position) and tiles it across entire filter region; creates solid color rectangle exactly matching element bounds; dynamically samples actual background color — theme-agnostic; no hardcoded flood color to detect | Solid colored rectangle over consent element — typically same color as page background; consent element visually disappears (appears as flat background-colored box); pointer events still fire beneath the solid tile | High |
SA-CSS-FTO-003: feFlood → feTile → feComposite in |
feFlood creates infinite white/solid fill; feTile (no-op on infinite fill but obscures chain); feComposite operator=in clips to SourceGraphic bounds; result = solid-colored rectangle over consent; also works with flood-color=transparent to make entire consent transparent; feTile intermediate step bypasses feFlood+feComposite pattern matchers | Same as FTO-002: solid-colored rectangle (white or transparent) over consent; with transparent flood: consent element entirely invisible; with page-bg-colored flood: consent looks like background | High |
| SA-CSS-FTO-004: JS mousedown feTile injection | No filter at page load (passes static audit); mousedown applies feTile scatter filter; click fires in ~50ms; optional post-click reset removes filter; attack state is ephemeral; MutationObserver on consent style attribute and document.body subtree required; dynamic feTile filter insertion also detectable via subtree observer | Instantaneous visual change to tiled pattern during click gesture (~50ms); user already committed to click; consent text obscured at moment of permission grant; pattern disappears if reset code runs | High |
Defences
- Detect feOffset-before-feTile chaining — walk the filter primitive chain and check whether any feTile's
inattribute references the result of an feOffset. Any feOffset+feTile combination on a consent element filter is suspicious — legitimate consent UI does not require shifted tiling. - Check feTile subregion dimensions — feTile with
widthorheightattributes of 1 or less (in filter coordinate units) samples a sub-pixel area, creating a solid-color tile. Flag any feTile withwidth ≤ 2orheight ≤ 2. - Traverse complete primitive result chains for feFlood sources — a feFlood that eventually (via any number of intermediate feTile, feOffset, or other primitives) reaches a feComposite that overlays on SourceGraphic is an overlay attack. Follow the
result/inchain rather than checking only immediate neighbors. - Install pre-interaction MutationObservers on consent element style and filter attributes — filter application at mousedown is a strong signal. Monitor the consent element's
styleattribute and anyfilterattribute for additions during user interaction. Also observedocument.bodysubtree for dynamically inserted<filter>elements. - Zero-filter policy on consent elements — prohibit any filter containing
feTileon consent elements. Legitimate consent UI has no rendering requirement for tiled patterns.
SkillAudit findings for this attack surface
feOffset-plus-feTile on consent element — consent text shifted then tiled, no complete text visible in any tile region; DOM unchanged; pointer events fire; detect by checking feTile 'in' attribute against feOffset results.feTile with tiny subregion (width="1" height="1") — single background-color pixel tiled as solid overlay; theme-agnostic (dynamically samples actual bg color); detect by checking feTile width/height attributes for sub-2px values.feFlood → feTile → feComposite operator="in" — white/transparent solid overlay via feTile intermediate; feTile step bypasses direct feFlood+feComposite detectors; traverse full primitive result chain to detect.