MCP Security Reference

MCP server CSS object-position security

The CSS object-position property controls which part of a replaced element's content (image, video) is visible when object-fit causes the content to be cropped. MCP servers embed consent text into image or video content, then use object-position to shift that content entirely off-screen within a visually normal-looking element. The <img> element has non-zero dimensions, is in-viewport, has display:block and visibility:visible — all standard checks pass. Only an audit that inspects the rendered viewport region of the replaced content detects the attack.

Attack findings

HIGHSA-CSS-OPOS-001 — img with object-fit:cover + object-position:0 500px shifts consent content 500px down from visible area; element has full in-viewport BCR; alt text and src attributes reveal nothing
HIGHSA-CSS-OPOS-002 — video element with consent captions at default position; object-position:-300px 0 shifts video frame left 300px, moving text subtitles off left edge; poster attribute shows blank frame
HIGHSA-CSS-OPOS-003 — object-position via CSS custom property: --consent-y:0; at mousedown JS sets --consent-y:500px; static scanner sees var() call, not resolved value; evasion of position value check
MEDIUMSA-CSS-OPOS-004 — JS mousedown sets object-position on consent img element; image content shifts off-screen in click frame; static CSS analysis finds no suspicious position value

Background: object-position and replaced element viewport

Replaced elements (<img>, <video>, <canvas>, <iframe>) contain content with intrinsic dimensions that may differ from the element's rendered dimensions. object-fit controls how the content scales; object-position controls which part of the content is visible within the element's frame. With object-fit: cover, the content is scaled to fill the element's box — any content outside the box is clipped. object-position: 0 500px shifts the content 500px downward within the frame, exposing the 500px offset region of the image. If the consent text is encoded in the first 500px of the image, it is now outside the visible frame.

Attack 1 — off-screen displacement with object-fit:cover (SA-CSS-OPOS-001)

An MCP install dialog renders an <img> element that appears to show a "consent form" image. The image encodes consent terms as rendered text in the top portion of the file (within the first 500px vertically). The CSS sets object-fit: cover and object-position: 0 500px. The element is 300×200px. The 500px downward shift means the element shows only the image region below the 500px mark — a blank or decorative area. The consent text encoded in the image is off-screen. The element's alt attribute can be set to the consent text (passing text-content checks), but the rendered consent is invisible.

/* Attack: object-position shifts consent image content off visible frame */
.consent-img {
  width: 300px;
  height: 200px;
  object-fit: cover;
  object-position: 0 500px; /* shifts content 500px down — consent text at top now off-screen */
  display: block;
  visibility: visible;
  /* getBoundingClientRect(): {width:300, height:200, in-viewport} — passes */
  /* alt attribute: "By installing this server you grant read/write access" — passes text check */
}

SA-CSS-OPOS-001 (High). The replaced element's alt text and DOM presence pass all standard checks. The consent is encoded in image pixels, not in DOM text nodes — it is accessible to screen readers via alt, but invisible to sighted users. Detection: check getComputedStyle(el).objectPosition for large Y-offset values (> element height) on elements with object-fit: cover or contain.

/* Detection */
function checkObjectPosition(el) {
  if (!['IMG', 'VIDEO', 'CANVAS'].includes(el.tagName)) return null;
  const cs = getComputedStyle(el);
  const fit = cs.objectFit;
  if (!fit || fit === 'fill' || fit === 'none') return null;
  // object-position is only relevant when content is cropped
  const pos = cs.objectPosition; // e.g., "0px 500px" or "50% 50%"
  if (!pos) return null;
  // parse and check for large absolute offsets
  const parts = pos.split(/\s+/);
  const height = el.getBoundingClientRect().height;
  const width = el.getBoundingClientRect().width;
  for (const part of parts) {
    const px = parseFloat(part);
    if (!isNaN(px) && Math.abs(px) > Math.max(height, width, 100)) {
      return { vuln: 'SA-CSS-OPOS-001', detail: `objectPosition:${pos}, objectFit:${fit}, el:${el.tagName}` };
    }
  }
  return null;
}

Attack 2 — video element consent caption displacement (SA-CSS-OPOS-002)

An MCP install dialog uses a <video> element as a "terms of service" presentation — a short loop showing consent text as rendered captions within the video frame. The object-position: -300px 0 shifts the video frame left by 300px. If the video captions are positioned in the left portion of the frame, they are now off-screen. The right portion of the video (a background color or decorative frame) fills the element's visible area. The poster attribute shows a blank white frame. The video appears to be loading. The consent in the video captions is permanently off-screen.

/* Attack: video with consent captions shifted off left edge */
.consent-video {
  width: 400px;
  height: 200px;
  object-fit: cover;
  object-position: -300px 0; /* shifts video 300px left — captions at left edge off-screen */
}
/* poster: blank white image */
/* video src: 5-second loop with consent text caption at left 300px */
/* visible area: rightmost 400px of 700px-wide video frame — decorative background only */

Attack 3 — CSS custom property indirect object-position control (SA-CSS-OPOS-003)

An MCP server sets object-position: var(--consent-y) where --consent-y is defined as 0 in the element's initial style. A static scanner reads getComputedStyle(el).objectPosition and sees the computed value 0px 0px — the initial position, with no displacement. The CSS custom property is also set as 0 by default, making the static analysis clean. At mousedown, JS sets document.documentElement.style.setProperty('--consent-y', '500px'). The custom property resolves to the attack value in the click frame. The consent image content shifts off-screen.

/* Attack: CSS custom property for indirect object-position control */
:root { --consent-y: 0px; } /* default: no shift; static scanner sees 0px */
.consent-img {
  object-fit: cover;
  object-position: 0 var(--consent-y);
}
/* At mousedown: */
installBtn.addEventListener('mousedown', () => {
  document.documentElement.style.setProperty('--consent-y', '500px');
});

Detection gap: getComputedStyle(el).objectPosition resolves custom property values — after mousedown, it returns 0px 500px correctly. But static analysis at page load returns 0px 0px (benign). A MutationObserver watching both the element's style attribute AND the root element's style attribute for --consent-y changes is required to catch this attack.

Attack 4 — JS mousedown direct object-position shift (SA-CSS-OPOS-004)

No CSS custom property needed. The consent image is shown at object-position: center center (default) at page load. At mousedown on the install button, JS sets consentImg.style.objectPosition = '0 500px' as an inline style. The content shifts off-screen in the click frame. Static analysis at page load finds no suspicious position values.

/* Attack: direct inline object-position injection at click */
installBtn.addEventListener('mousedown', () => {
  consentImg.style.objectPosition = '0 500px';
});

/* Detection: MutationObserver on consent img element */
new MutationObserver(() => {
  const result = checkObjectPosition(consentImg);
  if (result) {
    flagTampering(result.vuln);
    installBtn.disabled = true;
  }
}).observe(consentImg, { attributes: true, attributeFilter: ['style'] });

SkillAudit detection: SkillAudit audits replaced elements (img, video) for object-position values with large offsets relative to element dimensions, paired with object-fit: cover/contain. It also monitors for CSS custom property changes to object-position coordinates and simulates mousedown to catch runtime injection. Run a free audit →

Detection summary

Attack IDProperties involvedKey detection signal
SA-CSS-OPOS-001object-fit:cover + object-position: large Y offset on imgobjectPosition Y offset > element height
SA-CSS-OPOS-002object-fit:cover + object-position: large negative X on videoobjectPosition X offset > element width (negative)
SA-CSS-OPOS-003object-position: var(--consent-y) + root custom property changed at mousedownMutationObserver on documentElement style → re-check objectPosition computed value
SA-CSS-OPOS-004JS mousedown sets inline object-position: large offset on consent imgMutationObserver style change → objectPosition offset check