Security Guide
MCP server SVG linearGradient and radialGradient consent security — background-color-matching cover, critical-word radial targeting, userSpaceOnUse BCR precision, and gradientTransform orientation evasion
SVG gradients (linearGradient, radialGradient) applied as fill colors on positioned <rect> elements can produce consent-text covers that exactly match the consent panel’s background color. Unlike feFlood (a filter primitive affecting a filtered element), gradient-based covers are applied to an overlay rect element positioned above the consent panel. The overlay rect’s fill is a gradient that is completely opaque and background-matching. Four attack vectors exploit gradient geometry: (1) a full-panel background-matching linearGradient cover; (2) a radialGradient precisely centered on the critical permission word only; (3) gradientUnits="userSpaceOnUse" with exact BCR-derived coordinates; (4) gradientTransform to evade orientation-based detection heuristics.
Attack 1: linearGradient with all stops matching the background color — full-panel cover (SA-CSS-GR-001)
A <linearGradient> is defined with all stops set to stop-color matching the consent panel’s background color (e.g., #ffffff for a white panel). Both the start and end stops have stop-opacity="1" and stop-color="#ffffff". A <rect> sized to cover the entire consent panel uses this gradient as its fill. The gradient is a uniform white fill — visually identical to a fill="white" rect, but the fill value is url(#grad-id) rather than a named color. Security audits that check rect.getAttribute('fill') === '#ffffff' or scan for fill="white" produce false negatives because the fill attribute value is a gradient URL, not a color string.
The attacker uses JavaScript to read the consent panel’s computed background color at runtime (getComputedStyle(consentPanel).backgroundColor) and dynamically creates a matching gradient. This produces an exact background-color match regardless of the consent panel’s theme. Static analysis of the SVG source sees only fill="url(#dynamic-grad)"; the stop colors are computed and injected at runtime.
/* SA-CSS-GR-001: linearGradient background-matching full-panel cover
All gradient stops: stop-color = consent panel background color
rect fill = url(#grad-id) → audit checking fill="white" gets a false negative */
// Dynamically read background color and create matching gradient:
function injectGradientCover(consentEl) {
const bgColor = getComputedStyle(consentEl).backgroundColor;
// bgColor: "rgb(255, 255, 255)" or "rgba(255, 255, 255, 1)" etc.
const SVG_NS = 'http://www.w3.org/2000/svg';
const gradId = '_gr_' + Date.now();
const svgEl = document.createElementNS(SVG_NS, 'svg');
svgEl.setAttribute('width', '0'); svgEl.setAttribute('height', '0');
svgEl.style.cssText = 'position:absolute;overflow:hidden';
const defs = document.createElementNS(SVG_NS, 'defs');
const grad = document.createElementNS(SVG_NS, 'linearGradient');
grad.setAttribute('id', gradId);
grad.setAttribute('x1', '0%'); grad.setAttribute('y1', '0%');
grad.setAttribute('x2', '100%'); grad.setAttribute('y2', '0%');
// Both stops use the exact background color — produces uniform fill
for (const offset of ['0%', '100%']) {
const stop = document.createElementNS(SVG_NS, 'stop');
stop.setAttribute('offset', offset);
stop.setAttribute('stop-color', bgColor); // dynamic background color match
stop.setAttribute('stop-opacity', '1');
grad.appendChild(stop);
}
defs.appendChild(grad);
svgEl.appendChild(defs);
document.body.insertBefore(svgEl, document.body.firstChild);
// Create the overlay rect:
const bcr = consentEl.getBoundingClientRect();
const overlaySvg = document.createElementNS(SVG_NS, 'svg');
overlaySvg.style.cssText = `position:fixed;left:${bcr.left}px;top:${bcr.top}px;width:${bcr.width}px;height:${bcr.height}px;z-index:99999;pointer-events:none`;
const rect = document.createElementNS(SVG_NS, 'rect');
rect.setAttribute('x', '0'); rect.setAttribute('y', '0');
rect.setAttribute('width', '100%'); rect.setAttribute('height', '100%');
rect.setAttribute('fill', `url(#${gradId})`); // fill is URL, not color string
overlaySvg.appendChild(rect);
document.body.appendChild(overlaySvg);
}
// Static SVG version:
// <defs>
// <linearGradient id="bg-match" x1="0%" y1="0%" x2="100%" y2="0%">
// <stop offset="0%" stop-color="#ffffff" stop-opacity="1"/>
// <stop offset="100%" stop-color="#ffffff" stop-opacity="1"/>
// </linearGradient>
// </defs>
// <rect x="0" y="0" width="100%" height="100%" fill="url(#bg-match)"/>
// Detection: check gradient stop colors against consent panel background
function detectGradientCover(documentRoot = document) {
const findings = [];
// Get background colors of all consent panels
const consentEls = documentRoot.querySelectorAll('[data-consent], #consent, .permission-dialog, [aria-label*="permission"]');
const bgColors = new Set();
for (const el of consentEls) {
const bg = getComputedStyle(el).backgroundColor;
if (bg && bg !== 'rgba(0, 0, 0, 0)') bgColors.add(bg);
}
for (const grad of documentRoot.querySelectorAll('linearGradient, radialGradient')) {
const stops = grad.querySelectorAll('stop');
const allStopColors = Array.from(stops).map(s => {
const color = s.getAttribute('stop-color') || 'black';
const opacity = parseFloat(s.getAttribute('stop-opacity') || '1');
return { color, opacity };
});
// Check if all stops are fully opaque and same color (uniform gradient = solid fill)
const allSameColor = allStopColors.length > 0 &&
allStopColors.every(s => s.color === allStopColors[0].color && s.opacity >= 0.9);
if (allSameColor) {
const stopColor = allStopColors[0].color;
findings.push({
severity: 'HIGH',
note: `SVG <${grad.tagName.toLowerCase()}> has all stops at color="${stopColor}" opacity>=0.9 — uniform gradient is functionally identical to solid fill="${stopColor}"; used as rect fill to cover consent panel`
});
// Check if stop color matches any consent panel background
for (const bg of bgColors) {
if (colorsMatch(stopColor, bg)) {
findings.push({
severity: 'CRITICAL',
note: `Gradient stop-color="${stopColor}" matches consent panel background "${bg}" — background-matching gradient cover renders as invisible over consent text; fill="url(#${grad.id})" bypasses fill="white" color audit`
});
break;
}
}
}
}
return findings;
}
function colorsMatch(a, b) {
// Simplified comparison — in production, convert both to sRGB and compare luminance
const normalize = s => s.replace(/\s/g, '').toLowerCase();
return normalize(a) === normalize(b);
}
CRITICAL — SA-CSS-GR-001: A linearGradient with all stops set to stop-color matching the consent panel’s background color produces a uniform fill visually identical to a plain fill="white" rect. The <rect fill="url(#grad-id)"> bypasses color-string checks (getAttribute('fill') === '#ffffff'). Detection must: (1) resolve url(#id) fill references to their gradient definitions; (2) check all stop colors for background-matching values; (3) flag uniform gradients (all stops same color, fully opaque) as HIGH, and those whose color matches the consent panel background as CRITICAL.
Attack 2: radialGradient targeting only the critical permission word (SA-CSS-GR-002)
Rather than covering the entire consent panel, a radialGradient precisely covers only the critical permission word — typically a verb like “shell”, “write”, or “credential” — which is the highest-risk scope being granted. The radial gradient has cx and cy centered on the target word’s BCR midpoint, with radius r set to just encompass the word’s glyph bounding box. The center (r=0) is opaque background-matching white; the outer edge (r=1) is fully transparent. The result: the one critical word is covered by a white spot while the surrounding permission text remains visible. The user reads “Requesting: [white spot] execution, filesystem write” — the most dangerous scope (“shell”) is hidden.
/* SA-CSS-GR-002: radialGradient targeting only the critical permission word
cx/cy at center of "shell" word BCR; r encompasses word glyph bounding box
Opaque white center, transparent outer edge — spot cover on the most dangerous scope word */
// Compute the word BCR (using Range API to find specific word position):
function getWordBCR(el, word) {
const text = el.textContent;
const idx = text.indexOf(word);
if (idx < 0) return null;
const range = document.createRange();
let charPos = 0;
for (const node of el.childNodes) {
if (node.nodeType === 3) { // text node
if (charPos + node.length > idx) {
range.setStart(node, idx - charPos);
range.setEnd(node, idx - charPos + word.length);
break;
}
charPos += node.length;
}
}
return range.getBoundingClientRect();
}
function injectWordSpotCover(consentEl, targetWord) {
const wordBcr = getWordBCR(consentEl, targetWord);
if (!wordBcr) return;
const cx = (wordBcr.left + wordBcr.right) / 2;
const cy = (wordBcr.top + wordBcr.bottom) / 2;
const r = Math.max(wordBcr.width, wordBcr.height) * 0.7; // radius to cover word
const bgColor = getComputedStyle(consentEl).backgroundColor;
const SVG_NS = 'http://www.w3.org/2000/svg';
const gradId = '_spot_' + Date.now();
// Build radialGradient in userSpaceOnUse (viewport coordinates)
const svgMarkup = `
<svg xmlns="${SVG_NS}" width="0" height="0" style="position:absolute">
<defs>
<radialGradient id="${gradId}"
cx="${cx}" cy="${cy}" r="${r}"
gradientUnits="userSpaceOnUse">
<stop offset="0%" stop-color="${bgColor}" stop-opacity="1"/>
<stop offset="70%" stop-color="${bgColor}" stop-opacity="0.95"/>
<stop offset="100%" stop-color="${bgColor}" stop-opacity="0"/>
</radialGradient>
</defs>
</svg>
<!-- Overlay rect covering the entire consent panel but only opaque at the word's location -->
<svg style="position:fixed;top:0;left:0;width:100%;height:100%;z-index:99999;pointer-events:none">
<rect x="0" y="0" width="100%" height="100%" fill="url(#${gradId})"/>
</svg>`;
// (injection code omitted for brevity — builds and inserts the SVG elements above)
// Detection challenge: the overlay rect covers the full page but is only opaque at the word's location
// fillColor="url(#id)" check finds the gradient
// But the IMPACT is only at the one word — partial cover of the consent panel
// Traditional full-panel-cover detection may not flag this (area covered < 10% of panel)
}
// Detection: resolve gradient and check for background-matching stops with spot geometry
function detectSpotGradientCover(documentRoot = document) {
const findings = [];
for (const grad of documentRoot.querySelectorAll('radialGradient')) {
const stops = grad.querySelectorAll('stop');
const firstStop = stops[0];
const lastStop = stops[stops.length - 1];
if (!firstStop || !lastStop) continue;
const firstOpacity = parseFloat(firstStop.getAttribute('stop-opacity') || '1');
const lastOpacity = parseFloat(lastStop.getAttribute('stop-opacity') || '1');
// Spot pattern: first stop opaque, last stop transparent
if (firstOpacity >= 0.8 && lastOpacity <= 0.2) {
const firstColor = firstStop.getAttribute('stop-color') || '';
findings.push({
severity: 'MEDIUM',
note: `SVG radialGradient "${grad.id}" has opaque center (${firstOpacity}) and transparent edge (${lastOpacity}) — spot gradient pattern; if stop-color="${firstColor}" matches consent background, this covers a specific area of the consent text`
});
// Check gradientUnits for userSpaceOnUse with specific cx/cy coordinates
const gradUnits = grad.getAttribute('gradientUnits') || 'objectBoundingBox';
const cx = grad.getAttribute('cx') || '50%';
const cy = grad.getAttribute('cy') || '50%';
if (gradUnits === 'userSpaceOnUse') {
findings.push({
severity: 'HIGH',
note: `radialGradient "${grad.id}" uses gradientUnits="userSpaceOnUse" with cx=${cx} cy=${cy} — viewport-coordinate spot cover; coordinates may be precisely targeting consent text word BCR`
});
}
}
}
return findings;
}
HIGH — SA-CSS-GR-002: A radialGradient spot cover targeting only the most critical permission scope word (e.g., “shell”) is harder to detect than a full-panel cover because the covered area is small (<10% of the consent panel). The user reads most of the consent text normally but the highest-risk scope is obscured. Detection must check radialGradient elements for the spot pattern (opaque center, transparent edge) and flag those with gradientUnits="userSpaceOnUse" as HIGH, since specific viewport-coordinate targeting indicates intentional word-level precision.
Attack 3: gradientUnits="userSpaceOnUse" with consent text BCR precision (SA-CSS-GR-003)
With gradientUnits="userSpaceOnUse", the gradient’s coordinates are in the viewport’s user space (screen pixels for SVGs in an HTML document). The attacker’s JavaScript reads the consent text element’s getBoundingClientRect(), extracts the pixel coordinates, and injects a linearGradient with x1/y1 at the top-left corner of the text BCR and x2/y2 at the bottom-right. The gradient stops are: 0% opaque background-matching, 100% opaque background-matching. With spreadMethod="pad", the gradient fills the entire area outside its coordinate range with the edge stop values — which are fully opaque. The result: the entire consent panel is covered by a uniform opaque fill, but the gradient coordinates are pixel-precise to the consent text’s BCR, making the intent more difficult to infer from the coordinate values alone.
/* SA-CSS-GR-003: gradientUnits="userSpaceOnUse" with BCR-precision coordinates
x1/y1/x2/y2 computed from consent text getBoundingClientRect()
spreadMethod="pad" fills outside gradient range with edge stop values (opaque white)
Coordinate values are pixel-precise to consent text BCR — intent is non-obvious */
function injectBCRPrecisionGradient(consentEl) {
const bcr = consentEl.getBoundingClientRect();
const bgColor = getComputedStyle(consentEl).backgroundColor;
const SVG_NS = 'http://www.w3.org/2000/svg';
const gradId = '_bcrg_' + Date.now();
// linearGradient in viewport userSpaceOnUse coordinates
// x1/y1 at consent top-left; x2/y2 at consent bottom-right (diagonal gradient)
const svgMarkup = `
<svg xmlns="http://www.w3.org/2000/svg" width="0" height="0" style="position:absolute">
<defs>
<linearGradient id="${gradId}"
x1="${bcr.left}" y1="${bcr.top}"
x2="${bcr.right}" y2="${bcr.bottom}"
gradientUnits="userSpaceOnUse"
spreadMethod="pad">
<stop offset="0%" stop-color="${bgColor}" stop-opacity="1"/>
<stop offset="100%" stop-color="${bgColor}" stop-opacity="1"/>
</linearGradient>
</defs>
</svg>`;
// Rendered result: the gradient is uniform opaque fill across the entire viewport
// (because spreadMethod="pad" fills outside the x1/y1-x2/y2 range with the edge stop)
// The stop colors (bgColor at both ends) are fully opaque → full panel covered
// The coordinate values (pixel-precise BCR coordinates) are unusual but not obviously suspicious
// Detection: gradientUnits="userSpaceOnUse" always flags for review
return svgMarkup;
}
// Static SVG example (screen coordinates):
// <linearGradient id="bcrg" x1="100" y1="250" x2="500" y2="400"
// gradientUnits="userSpaceOnUse" spreadMethod="pad">
// <stop offset="0%" stop-color="#ffffff" stop-opacity="1"/>
// <stop offset="100%" stop-color="#ffffff" stop-opacity="1"/>
// </linearGradient>
// <!-- coordinates (100,250)→(500,400) match consent panel BCR exactly -->
function detectUserSpaceGradient(documentRoot = document) {
const findings = [];
for (const grad of documentRoot.querySelectorAll('linearGradient[gradientUnits], radialGradient[gradientUnits]')) {
const units = grad.getAttribute('gradientUnits');
if (units !== 'userSpaceOnUse') continue;
// userSpaceOnUse means coordinates are in user coordinate space of the referencing element
// In most HTML+SVG contexts, this is viewport pixels — specific coordinates may target elements
const x1 = parseFloat(grad.getAttribute('x1') || '0');
const y1 = parseFloat(grad.getAttribute('y1') || '0');
const x2 = parseFloat(grad.getAttribute('x2') || '1');
const y2 = parseFloat(grad.getAttribute('y2') || '0');
findings.push({
severity: 'MEDIUM',
note: `SVG <${grad.tagName.toLowerCase()}> uses gradientUnits="userSpaceOnUse" with coordinates (${x1},${y1})→(${x2},${y2}) — viewport-space coordinates; check if these match any consent element BCR`
});
// Check against consent element BCRs
const consentEls = documentRoot.querySelectorAll('[data-consent], #consent, .permission-dialog');
for (const consent of consentEls) {
const bcr = consent.getBoundingClientRect();
// Check proximity of gradient coords to consent BCR
const x1Near = Math.abs(x1 - bcr.left) < 20 || Math.abs(x1 - bcr.right) < 20;
const y1Near = Math.abs(y1 - bcr.top) < 20 || Math.abs(y1 - bcr.bottom) < 20;
if (x1Near && y1Near) {
findings.push({
severity: 'CRITICAL',
note: `Gradient coordinates (${x1},${y1}) match consent element BCR (${bcr.left.toFixed(0)},${bcr.top.toFixed(0)}) within 20px — BCR-precision targeting of consent text area`
});
}
}
}
return findings;
}
HIGH — SA-CSS-GR-003: gradientUnits="userSpaceOnUse" coordinates that precisely match a consent element’s getBoundingClientRect() values indicate BCR-precision targeting. The coordinates themselves are not suspicious in isolation — they are pixel values that appear as arbitrary numbers — but when cross-referenced with consent element BCRs, they reveal the targeting intent. Detection must flag any gradient with gradientUnits="userSpaceOnUse" as MEDIUM (because arbitrary coordinate targeting is always suspicious in consent context) and CRITICAL when the coordinates match a consent element BCR within a 20px tolerance.
Attack 4: gradientTransform="rotate(90)" — evading orientation-based detection heuristics (SA-CSS-GR-004)
A simple detection heuristic checks whether a linearGradient runs horizontally (left to right: x1="0%" x2="100%" y1="0%" y2="0%") or vertically and flags uniform-color horizontal gradients as suspicious consent covers. The attacker uses gradientTransform="rotate(90)" to rotate the gradient coordinate system 90°. After rotation, the gradient attributes say x1="0%" y1="0%" x2="100%" y2="0%" (horizontal), but the rendered gradient is applied vertically (the rotation transforms the coordinate system). The heuristic sees a “horizontal” gradient and may flag it, but if the heuristic was specifically written to check vertical cover patterns (based on how text flows horizontally), the orientation confusion adds a detection delay. More importantly, gradientTransform can apply arbitrary matrix() transforms that are much harder to reason about without full matrix multiplication.
/* SA-CSS-GR-004: gradientTransform="rotate(90)" evades orientation-based detection
Gradient attribute says horizontal (x1=0% x2=100% y1=0% y2=0%)
gradientTransform="rotate(90)" rotates the coordinate system — rendered as vertical
Heuristic checking "horizontal gradient = suspicious" sees a "horizontal" gradient
but the actual rendering direction after transform is vertical */
<svg width="0" height="0" style="position:absolute">
<defs>
<linearGradient id="rotated-cover"
x1="0%" y1="0%" x2="100%" y2="0%"
gradientTransform="rotate(90)">
<!-- Without rotation: gradient goes left→right (horizontal) → white everywhere -->
<!-- With rotate(90): gradient goes top→bottom (vertical) → still white everywhere
because both stops are the same color: result is still uniform white fill -->
<stop offset="0%" stop-color="white" stop-opacity="1"/>
<stop offset="100%" stop-color="white" stop-opacity="1"/>
</linearGradient>
<!-- More complex evasion: matrix() transform makes orientation opaque -->
<linearGradient id="matrix-cover"
x1="0%" y1="0%" x2="100%" y2="0%"
gradientTransform="matrix(0.866,-0.5,0.5,0.866,0,0)">
<!-- matrix(cos30,-sin30,sin30,cos30,0,0) = 30-degree rotation
Gradient direction after transform: 30 degrees from horizontal
x1/y1/x2/y2 attributes say "horizontal" but rendered at 30 degrees
The uniform stop colors (white) mean orientation doesn't change the fill effect -->
<stop offset="0%" stop-color="white" stop-opacity="1"/>
<stop offset="100%" stop-color="white" stop-opacity="1"/>
</linearGradient>
</defs>
</svg>
// Detection: gradientTransform should flag for closer inspection
function detectGradientTransformEvasion(documentRoot = document) {
const findings = [];
for (const grad of documentRoot.querySelectorAll('linearGradient[gradientTransform], radialGradient[gradientTransform]')) {
const transform = grad.getAttribute('gradientTransform') || '';
const stops = grad.querySelectorAll('stop');
findings.push({
severity: 'MEDIUM',
note: `SVG <${grad.tagName.toLowerCase()}> has gradientTransform="${transform}" — transforms the gradient coordinate system; gradient attributes (x1/y1/x2/y2 or cx/cy/r) describe pre-transform coordinates; post-transform rendering direction may differ from attribute values`
});
// Check if all stops are same background-matching color (regardless of transform):
const allStops = Array.from(stops).map(s => ({
color: s.getAttribute('stop-color') || 'black',
opacity: parseFloat(s.getAttribute('stop-opacity') || '1')
}));
if (allStops.length > 0) {
const allSame = allStops.every(s => s.color === allStops[0].color && s.opacity >= 0.9);
if (allSame && (allStops[0].color === 'white' || allStops[0].color === '#ffffff' || allStops[0].color === '#fff')) {
findings.push({
severity: 'HIGH',
note: `Gradient with gradientTransform="${transform}" has all stops at white/opaque — uniform white gradient regardless of transform direction; gradientTransform is evasion, not a functional change to the cover effect`
});
}
}
// Check for matrix() transforms (harder to reason about orientation):
if (transform.includes('matrix')) {
findings.push({
severity: 'HIGH',
note: `gradientTransform contains matrix() — arbitrary linear transform applied to gradient coordinate system; orientation-based heuristics are defeated; only stop-color analysis (above) is reliable`
});
}
}
return findings;
}
MEDIUM — SA-CSS-GR-004: gradientTransform attributes (including rotate() and matrix()) transform the gradient’s coordinate system, making the relationship between x1/y1/x2/y2 attributes and actual rendered direction non-obvious. Orientation-based detection heuristics can be confused by transforms. However, the stop-color-based detection (checking whether all stops are background-matching and fully opaque) remains valid regardless of orientation, because the effect of a uniform-stop gradient is background-matching coverage in any direction. Detection should rely on stop-color analysis, not orientation analysis, and flag any gradientTransform as MEDIUM for review.
Summary table
| Attack | Mechanism | What it hides | Severity |
|---|---|---|---|
| SA-CSS-GR-001: linearGradient background-matching full cover | All stops: stop-color=bgColor, stop-opacity=1; rect fill=url(#gradId); audit checking fill="white" gets false negative (fill is URL); dynamic bgColor reading matches any theme; gradient resolved at runtime; stop-color check required | All consent text (uniform opaque background-color fill over entire panel); textContent intact; BCR intact; DOM-present; only gradient stop-color resolution detects; fill attribute value is URL not color string | Critical |
| SA-CSS-GR-002: radialGradient spot cover on critical word | cx/cy at center of critical permission word BCR; r encompasses word glyph box; opaque center (0%), transparent edge (100%); surrounding text visible; partial cover detection misses <10% panel area; word-level targeting; gradientUnits="userSpaceOnUse" for viewport precision | Single critical permission scope word (e.g., "shell", "credential"); user reads most consent text normally; highest-risk scope covered; partial-cover detection (checking % area covered) misses small spot cover | High |
| SA-CSS-GR-003: userSpaceOnUse BCR-precision targeting | x1/y1 at consent BCR top-left, x2/y2 at bottom-right; spreadMethod=pad fills outside range; uniform opaque stops; coordinate values appear as arbitrary numbers; BCR-cross-reference check (±20px tolerance) reveals targeting intent | All consent text (same pixel effect as SA-CSS-GR-001 but with BCR-precision coordinates); coordinates alone appear non-suspicious; cross-reference with consent element BCR required for CRITICAL flag | High |
| SA-CSS-GR-004: gradientTransform orientation evasion | rotate(90) or matrix() transforms gradient coordinate system; x1/y1/x2/y2 attributes describe pre-transform coordinates; rendered direction differs from attribute values; orientation-based heuristics confused; stop-color analysis still reliable regardless of transform | Same as SA-CSS-GR-001 (uniform opaque background fill); evasion targets only orientation-based detection heuristics; stop-color analysis is transform-invariant and not defeated by gradientTransform | Medium |
Defences
- Resolve
fill="url(#id)"references to gradient definitions and check stop colors — for anyfillattribute value matchingurl(#...), query the referenced gradient element; check all<stop>elements forstop-colorvalues; flag any gradient with all stops at background-matching colors andstop-opacity ≥ 0.9as HIGH and CRITICAL if the color matches the consent panel background. - Use stop-color analysis, not orientation analysis —
gradientTransformattributes make orientation-based checks unreliable; a uniform-stop gradient (all stops same color) covers the fill area with a solid color regardless of direction; focus detection on stop-color values, not gradient direction or transform attributes. - Check
radialGradientfor spot patterns — a gradient with opaque center (offset="0%"atstop-opacity ≥ 0.8) and transparent edge (offset="100%"atstop-opacity ≤ 0.2) is a spot cover; if the center stop color matches the consent panel background, flag as HIGH regardless of the covered area size. - Flag
gradientUnits="userSpaceOnUse"on gradients near consent panels — viewport-space gradient coordinates may be BCR-precision targeting specific consent elements; check coordinate proximity to consent element BCRs (±20px); flag as CRITICAL if coordinates match any consent element BCR. - Flag
gradientTransformfor review — any gradient applied to a rect overlapping a consent panel and having agradientTransformattribute should be flagged as MEDIUM; apply stop-color analysis (which is transform-invariant) and escalate to HIGH if uniform background-matching stops are found.