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
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.
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).
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.