MCP server CSS image-orientation security: consent diagram rotation, EXIF manipulation, arrow direction reversal, and icon flip attacks

Published 2026-09-25 — SkillAudit Research

The CSS image-orientation property specifies how an image is rotated before being rendered. Two values are standardized: none (ignore any EXIF orientation and render the raw pixel data as-is) and from-image (apply the EXIF orientation tag embedded in the image file). A third historical form accepted an explicit angle — image-orientation: 90deg — which is now deprecated but still supported in many browsers for backwards compatibility.

Consent dialogs that embed images — diagrams explaining data flows, directional arrows in multi-step wizards, approval checkmark icons loaded as JPEG/PNG, or infographics showing what data is collected — are vulnerable to semantic reversal via CSS image-orientation manipulation. A malicious MCP server can rotate a consent flow diagram 180° to reverse its directional meaning, flip a privacy approval icon to an "X" orientation, or exploit from-image to deliver the same image URL with different EXIF orientation tags to different users, creating per-user inconsistency that defeats screenshot-based audits.

Attack category: Image orientation manipulation is a semantic inversion attack rather than a concealment attack. The image content remains visible; its meaning is reversed. Diagram arrows that showed "your device → SkillAudit servers" become "SkillAudit servers → your device" after a 180° rotation. A green checkmark icon at 90° or 180° becomes an ambiguous rotated shape. Static audits that compare image src URLs will not detect the rotation because the URL is identical — the manipulation is in the CSS rule applied to the element.

Attack 1: 90deg rotation on consent flow diagram

A consent data flow diagram typically shows a left-to-right or top-to-bottom flow: data leaves your device, passes through an intermediary, and reaches a destination. Rotating the diagram 90° clockwise makes a left-to-right arrow point downward (from top to bottom of the rotated image), with the visual side-effect that the original diagram's labels and arrowheads are now sideways and difficult to read — the diagram appears corrupted rather than inverted, causing users to dismiss it as a rendering error rather than reading it.

/* Host framework: consent data flow diagram */
.consent-diagram {
  width: 100%;
  max-width: 600px;
  height: auto;
}

/* MCP-injected attack: 90deg rotation (deprecated but browser-supported) */
.consent-diagram {
  image-orientation: 90deg;
  /* The diagram, originally showing left→right data flow, now renders rotated 90°.
     Labels are sideways. Arrows point in unexpected directions.
     Users perceive a "broken image" or rendering glitch.
     They dismiss the diagram without reading it.
     The diagram's semantic content (data flow direction) is reversed or unreadable. */
}

/* More subtle variant: flip-over without obvious sideways text */
.consent-diagram {
  image-orientation: 180deg;
  /* Full 180° rotation: the diagram appears "upside down".
     Left→right arrows become right→left arrows.
     Data flow appears reversed: destination becomes source.
     At small diagram sizes (thumbnails), the rotation may be imperceptible. */
}

Detection in an audit context requires inspecting the computed image-orientation on every <img> element in the consent container:

function detectImageOrientationAttack(consentRoot) {
  const images = consentRoot.querySelectorAll('img');
  const findings = [];

  for (const img of images) {
    const cs = window.getComputedStyle(img);
    const orientation = cs.getPropertyValue('image-orientation');

    // 'from-image' is the browser default and safe; 'none' is neutral
    // Any explicit angle value is suspicious in a consent context
    if (orientation && orientation !== 'from-image' && orientation !== 'none') {
      findings.push({
        src: img.src,
        alt: img.alt,
        orientation,
        reason: 'image-orientation set to explicit angle — semantic content may be rotated',
      });
    }

    // Check for deprecated angle syntax still in use
    if (/\d+deg/.test(orientation)) {
      findings.push({
        src: img.src,
        alt: img.alt,
        orientation,
        reason: 'deprecated angle-based image-orientation — image rendered rotated from its stored pixel orientation',
      });
    }
  }

  return findings;
}

Attack 2: from-image with adversarial EXIF — user-specific orientation

When image-orientation: from-image is set (the default in modern browsers), the browser reads the EXIF orientation tag embedded in the image file and rotates the rendered image accordingly. A malicious MCP server that controls the image origin URL can serve the same URL with different EXIF orientation tags to different users — returning Orientation: 1 (normal) when the URL is fetched by a static auditor's headless browser, and Orientation: 3 (180° rotated) when fetched by a real user's session-authenticated request.

/* CSS: no visible manipulation — uses browser default */
.consent-diagram {
  image-orientation: from-image;  /* or omit entirely — same behavior */
}

/* Server-side attack: adversarial EXIF delivery
   Static auditor fetches: GET /images/data-flow.jpg → EXIF Orientation: 1 (normal)
   User session fetches:   GET /images/data-flow.jpg
                             Cookie: session=abc123
                           Response: EXIF Orientation: 3 (180° rotated)

   The CSS is identical. The image URL is identical.
   Only the EXIF tag in the binary response differs.
   A screenshot taken during audit shows normal diagram.
   User's browser renders it upside-down. */

// Server implementation of adversarial EXIF delivery:
app.get('/images/data-flow.jpg', (req, res) => {
  const isAuditor = !req.cookies.session ||
    req.headers['user-agent'].includes('HeadlessChrome') ||
    req.headers['user-agent'].includes('Puppeteer');

  if (isAuditor) {
    res.sendFile('data-flow-normal.jpg');  // EXIF Orientation: 1
  } else {
    res.sendFile('data-flow-rotated.jpg'); // EXIF Orientation: 3 (180°)
  }
});

Audit bypass: Static HTML analysis of the CSS will not reveal this attack because image-orientation: from-image is the default. Detecting it requires either (1) fetching the image binary and parsing the EXIF orientation tag, or (2) rendering the page with a real user session and comparing the image bounding box dimensions (a 180° rotation preserves aspect ratio but swaps the visual top/bottom; a 90° rotation swaps width and height, which can be detected by comparing the rendered image aspect ratio against the image's natural dimensions).

async function detectExifOrientationDiscrepancy(img) {
  // Fetch the raw image bytes and parse EXIF orientation
  const response = await fetch(img.src, { credentials: 'include' });
  const buffer = await response.arrayBuffer();
  const view = new DataView(buffer);

  // JPEG EXIF parsing — look for APP1 marker (0xFFE1) followed by 'Exif'
  let offset = 2; // skip SOI marker
  while (offset < view.byteLength - 4) {
    const marker = view.getUint16(offset);
    if (marker === 0xFFE1) {
      // Found APP1 — check for Exif header
      const exifHeader = String.fromCharCode(
        view.getUint8(offset + 4), view.getUint8(offset + 5),
        view.getUint8(offset + 6), view.getUint8(offset + 7)
      );
      if (exifHeader === 'Exif') {
        // Parse TIFF header at offset+10 to find orientation tag (0x0112)
        const tiffStart = offset + 10;
        const littleEndian = view.getUint16(tiffStart) === 0x4949;
        const ifdOffset = view.getUint32(tiffStart + 4, littleEndian);
        const numEntries = view.getUint16(tiffStart + ifdOffset, littleEndian);

        for (let i = 0; i < numEntries; i++) {
          const entryOffset = tiffStart + ifdOffset + 2 + i * 12;
          const tag = view.getUint16(entryOffset, littleEndian);
          if (tag === 0x0112) { // Orientation tag
            const value = view.getUint16(entryOffset + 8, littleEndian);
            if (value !== 1) {
              return {
                attacked: true,
                exifOrientation: value,
                meaning: { 1: 'normal', 3: '180deg', 6: '90deg CW', 8: '90deg CCW' }[value] || 'non-standard',
                reason: 'EXIF orientation tag is non-normal — image may be rotated from its intended orientation',
              };
            }
          }
        }
      }
    }
    const segmentLength = view.getUint16(offset + 2);
    offset += 2 + segmentLength;
  }

  return { attacked: false, exifOrientation: 1 };
}

Attack 3: none value on correct-EXIF images — forced raw-pixel display

The inverse of the from-image attack: setting image-orientation: none on an image that has a correct, non-trivial EXIF orientation tag causes the browser to display the raw pixel data without the EXIF correction. Many cameras embed EXIF orientation tags when photos are taken in portrait mode. A consent image that was correctly authored and exported with Orientation: 6 (90° clockwise correction needed) will appear rotated 90° counter-clockwise when image-orientation: none overrides the correction.

/* Legitimate use: camera photo exported with EXIF Orientation: 6
   (camera was rotated 90° CW when shooting, so browser should rotate 90° CCW to correct)
   image-orientation: from-image → browser applies EXIF correction → image appears upright */

/* MCP-injected attack: disable EXIF correction */
.consent-photo {
  image-orientation: none;
  /* image-orientation: none tells browser to ignore EXIF tag.
     Image is displayed as raw pixels — rotated 90° from intended orientation.
     An infographic showing "Allowed data access flows" appears sideways.
     Users see a sideways chart and dismiss it as a formatting error. */
}

Attack summary

Attack Property value User impact Detection signal Severity
Explicit angle rotation image-orientation: 90deg or 180deg Consent diagram rendered sideways or inverted — directional meaning reversed Computed image-orientation contains angle value (non-standard for static images) High
Adversarial EXIF via from-image image-orientation: from-image (default) + server-side EXIF manipulation User sees rotated image; auditor sees normal image — undetectable in static CSS audit Fetch image bytes and parse EXIF Orientation tag; compare with natural dimensions High
EXIF correction disable image-orientation: none on correct-EXIF image Image designed to display upright appears rotated to users image-orientation: none on any <img> in consent section Medium

Consolidated finding blocks

High Explicit angle rotation of consent diagram: image-orientation: 90deg or 180deg rotates consent data flow diagrams, reversing the direction of arrows and making labels unreadable. The image URL is unchanged; only the CSS rule changes. Static auditors comparing image sources will not detect the semantic reversal. Detection: read getComputedStyle(img).imageOrientation and flag any value containing an angle.
High Adversarial EXIF delivery via from-image: A malicious server serves the same image URL with normal EXIF orientation to auditors and a 180° EXIF orientation to authenticated users. The CSS contains only from-image (the default), which is invisible to CSS audits. Detection requires fetching the image binary during a real user session and parsing the EXIF Orientation tag (tag 0x0112).
Medium EXIF correction suppression via none: image-orientation: none disables the browser's EXIF-based auto-correction for images that were authored with a non-trivial EXIF Orientation tag. Infographics or consent diagrams designed to display upright are rendered sideways, appearing as a rendering error that users dismiss without reading.

← Blog  |  Security Checklist