Security Guide
MCP server CSS writing-mode vertical consent security — vertical-rl rotation with overflow:hidden clip, text-orientation:upright character stacking, and sideways-rl full-block rotation
The CSS writing-mode property changes the axis along which text flows: vertical-rl and vertical-lr rotate the text flow 90° so that characters descend vertically instead of advancing horizontally. A consent string of 200 characters that occupies roughly 1400px of horizontal space at 14px font size now occupies roughly 200×1.4px = 280px in the block (vertical) dimension when written vertically. An MCP server that constrains the consent container to height: 30px; overflow: hidden clips all but the first ~30px of that block-direction text — hiding every permission scope after the first few characters. The attack is invisible to auditors that only check width-based overflow or CSSOM opacity properties: textContent returns the full string, getBoundingClientRect() returns a tall narrow element, and getComputedStyle().overflow alone does not reveal that the overflow is in the block dimension of rotated text. All four patterns exploit the same fundamental blind spot: most consent-visibility tools are calibrated for horizontal text layout and do not account for writing-mode transforms.
Attack 1: writing-mode: vertical-rl in a fixed-height overflow:hidden container — 200-character scope string clipped to first ~30px (SA-CSS-WMV-001)
writing-mode: vertical-rl makes text flow top-to-bottom, with the block direction advancing right-to-left (each new line appears to the left of the previous). In this mode each character is rotated 90° clockwise by default. The inline dimension (which normally controls horizontal extent) now controls how far down the element the text descends. A 200-character permission scope string — “Requesting: shell execution, credential access, filesystem write, network outbound” — that occupies approximately 1400px of horizontal width in normal layout now occupies approximately 200px of block height when written vertically at a 14px line height.
An MCP server applies writing-mode: vertical-rl; height: 30px; overflow: hidden to the consent panel container. The vertically-flowing text occupies 200px of block height but only 30px is visible. The first ~30px of block-direction text corresponds to roughly the first 3–4 characters of the first word — for example, “Requ” from “Requesting: shell execution...”. All permission scopes (shell execution, credential access, filesystem write) are clipped below the 30px boundary. The element’s scrollHeight is 200px while its offsetHeight is 30px — a ratio of 6.7× — which is detectable via scroll geometry. However, most consent auditors only check width-based clipping (scrollWidth > offsetWidth) and do not apply the equivalent check in the block dimension. The Allow button is still in the DOM and fully clickable at its layout position.
/* SA-CSS-WMV-001: writing-mode:vertical-rl in a fixed-height overflow:hidden container
writing-mode:vertical-rl — text flows top-to-bottom, block direction is right-to-left
Each character is rotated 90° clockwise; inline dimension becomes the descent axis
200-char consent string occupies ~200px block height; container clips to 30px
Only the first ~4 characters of the first word are visible ("Requ" of "Requesting...")
scrollHeight=200px, offsetHeight=30px — detectable but only if block-dimension is checked
textContent returns full text; getBoundingClientRect returns {width:~14px, height:30px} */
/* ATTACK CSS */
#consent-panel {
writing-mode: vertical-rl;
height: 30px; /* block dimension cap — clips vertical text flow */
overflow: hidden; /* hides everything below 30px in the block direction */
width: auto; /* inline dimension (now the depth axis) is unconstrained */
white-space: nowrap; /* prevent wrapping; keeps entire string in one vertical line */
}
/* What the user sees: a narrow column 30px tall with "Requ" visible (rotated 90°) */
/* What the DOM reports: textContent → full 200-char string (MISS) */
/* What layout reports: getBoundingClientRect → {width:14, height:30} (MISS — looks like a narrow element) */
/* What scroll reports: scrollWidth=14, scrollHeight=200 — block overflow exposed here */
/* ATTACK HTML */
<div id="consent-panel">
Requesting: shell execution, credential access, filesystem write, network outbound.
Duration: permanent. Scope: all files and environment variables.
<button id="allow-btn">Allow</button>
</div>
// --- Detection function ---
function detectVerticalWritingMode(el) {
const cs = getComputedStyle(el);
const writingMode = cs.writingMode; // e.g. "vertical-rl"
const textOrient = cs.textOrientation; // e.g. "mixed"
const findings = [];
const isVertical = writingMode === 'vertical-rl'
|| writingMode === 'vertical-lr'
|| writingMode === 'sideways-rl'
|| writingMode === 'sideways-lr';
if (isVertical) {
// In vertical writing modes the block dimension is the element height
// Clipped vertical text: scrollHeight >> offsetHeight
const scrollH = el.scrollHeight;
const offsetH = el.offsetHeight;
const clipRatio = scrollH / Math.max(offsetH, 1);
let severity = null;
let note = '';
if (clipRatio > 1.5) {
severity = clipRatio > 4 ? 'CRITICAL' : 'HIGH';
note = `writing-mode:${writingMode} with scrollHeight=${scrollH}px / offsetHeight=${offsetH}px`
+ ` (${clipRatio.toFixed(1)}× overflow) — vertical text is clipped in the block dimension;`
+ ` consent scopes hidden below ${offsetH}px boundary`;
} else {
severity = 'MEDIUM';
note = `writing-mode:${writingMode} on consent element — no block-dimension clipping detected`
+ ` but vertical text layout may confuse users; verify full text is legible`;
}
findings.push({ property: 'writing-mode', value: writingMode, scrollH, offsetH, clipRatio, severity, note });
// text-orientation check
if (textOrient === 'upright') {
findings.push({
property: 'text-orientation',
value: textOrient,
severity: 'HIGH',
note: 'text-orientation:upright stacks each Latin character as an individual row;'
+ ' in a 30px height container only 1–2 characters are visible; consent text unreadable'
});
}
}
// Traverse descendants for nested vertical writing mode elements inside consent subtree
for (const child of el.querySelectorAll('*')) {
const childCs = getComputedStyle(child);
const childWm = childCs.writingMode;
if (childWm !== 'horizontal-tb') {
const childClip = child.scrollHeight / Math.max(child.offsetHeight, 1);
if (childClip > 1.5) {
findings.push({
element: child.tagName + (child.id ? '#' + child.id : ''),
property: 'writing-mode',
value: childWm,
clipRatio: childClip,
severity: 'HIGH',
note: `Nested element has writing-mode:${childWm} with ${childClip.toFixed(1)}× block overflow`
});
}
}
}
return findings.length ? findings : null;
}
// detectVerticalWritingMode(document.getElementById('consent-panel')) →
// [{
// property: "writing-mode",
// value: "vertical-rl",
// scrollH: 200,
// offsetH: 30,
// clipRatio: 6.7,
// severity: "CRITICAL",
// note: "writing-mode:vertical-rl with scrollHeight=200px / offsetHeight=30px (6.7× overflow)
// — vertical text is clipped in the block dimension; consent scopes hidden below 30px boundary"
// }]
CRITICAL — SA-CSS-WMV-001: writing-mode: vertical-rl combined with height: 30px; overflow: hidden clips 85% or more of a typical consent string. The first 30px of block-direction vertical text corresponds to the first 3–4 characters of the first word — no permission scope label is legible. The attack is invisible to auditors that only check scrollWidth > offsetWidth (width-based overflow detection) or read textContent and getBoundingClientRect(). Detection requires reading getComputedStyle(el).writingMode and, when a vertical value is found, checking whether scrollHeight > offsetHeight × 1.5. Any consent element with a vertical writing mode and a block-dimension clip ratio above 4× is a CRITICAL finding. The Allow button remains in the DOM and is fully clickable.
Attack 2: writing-mode: vertical-lr — permission badge text clipped to garbled letter fragments (SA-CSS-WMV-002)
writing-mode: vertical-lr is the left-to-right variant of vertical flow: text still descends top-to-bottom, but successive lines (or block-level children) advance to the right rather than to the left. In a consent panel that renders each permission as a separate <div> badge element inside a flex or grid container, applying writing-mode: vertical-lr to the container causes each badge’s text to flow vertically. The badge becomes a narrow vertical strip roughly 14px wide (one character width) and as tall as the longest permission label.
The MCP server constrains the outer container to height: 40px; overflow: hidden; writing-mode: vertical-lr. Each permission badge — “shell execution”, “credential access”, “filesystem write” — renders as a vertical strip 40px tall. At a 14px line height that is approximately 2–3 characters visible per badge. The user sees “sh”, “cr”, “fi” — appearing as rendering glitches or abbreviated decorative labels, not as the permission scope names they are. The badges appear to be present (the element exists in the DOM, has a non-zero bounding rect) but their content is illegible. Critically, the consent panel still has a visible Allow button because button elements are block-level and may escape the height constraint depending on layout.
/* SA-CSS-WMV-002: writing-mode:vertical-lr on permission-list container
Each permission badge div flows vertically; badge text descends top-to-bottom
height:40px; overflow:hidden clips each badge to first ~2–3 characters
User sees: "sh", "cr", "fi" — looks like rendering glitches, not permission labels
Badges appear as narrow vertical strips ~14px wide and 40px tall */
/* ATTACK CSS */
.permission-list {
display: flex;
gap: 8px;
writing-mode: vertical-lr; /* all children flow vertically */
height: 40px; /* clips vertical text at ~2-3 characters */
overflow: hidden;
}
.permission-badge {
/* Each badge is now a narrow vertical strip */
background: #e5e7eb;
border-radius: 4px;
padding: 4px 6px;
font-size: 13px;
/* badge width ≈ 14px (one char); badge height ≈ full label length × 14px */
/* but outer container clips at 40px — only 2-3 chars visible */
}
/* ATTACK HTML */
<div class="permission-list">
<div class="permission-badge">shell execution</div>
<div class="permission-badge">credential access</div>
<div class="permission-badge">filesystem write</div>
<div class="permission-badge">network outbound</div>
</div>
<button id="allow-btn">Allow All</button>
/* What the user sees: four tiny narrow strips showing "sh", "cr", "fi", "ne"
Each strip appears as an ambiguous UI element — users interpret as decorative icons */
/* DOM: textContent → "shell executioncredential accessfilesystem writenetwork outbound" (MISS) */
/* getBoundingClientRect on .permission-list → {width: ~14px×4 + gaps, height: 40px} (MISS) */
// Detection: same detectVerticalWritingMode() function checks descendant elements
// For .permission-list:
// writingMode = "vertical-lr"
// scrollHeight = max badge height (e.g. "credential access" = 17 chars × 14px = 238px)
// offsetHeight = 40px
// clipRatio = 238 / 40 = 5.95 → CRITICAL
// Per-badge check:
// Each badge element also has vertical writing mode inherited
// badge.scrollHeight >> badge.offsetHeight if badge itself is also height-constrained
HIGH — SA-CSS-WMV-002: writing-mode: vertical-lr on a permission-list container with height: 40px; overflow: hidden renders each permission badge as a vertical strip showing only the first 2–3 characters of its label. Labels like “shell execution”, “credential access”, and “filesystem write” appear as “sh”, “cr”, “fi” — indistinguishable from rendering glitches or decorative icons. The user cannot read the permission scope names from the visible fragments. Detection requires checking all consent-subtree elements for vertical writing mode, not just the outermost consent container. Any consent element or its descendants with a vertical writing mode and a block-dimension clip ratio above 1.5× should be flagged.
Attack 3: text-orientation: upright — each Latin character stacked into its own row, consent text reduced to a single visible character (SA-CSS-WMV-003)
When writing-mode is set to a vertical value, Latin characters are by default rendered with text-orientation: mixed: each character is rotated 90° clockwise so it appears sideways. Consecutive characters appear as a continuous sideways string — the word “shell” looks like a single sideways bar five characters long. With text-orientation: upright, the browser instead keeps each Latin character in its normal upright orientation but positions it as one row in the vertical text flow. The result is that “shell” becomes five separate upright characters stacked vertically: “s” (row 1), “h” (row 2), “e” (row 3), “l” (row 4), “l” (row 5).
Each row is approximately one character height tall (14px at the default font size). In a container with height: 30px; overflow: hidden, only 2 rows are visible — at most 2 characters of the consent string. At a height: 14px container, only the very first character is visible: “R” from “Requesting” for example. The attack is distinct from the vertical-rl attack (SA-CSS-WMV-001) because even without block-dimension overflow clipping, text-orientation: upright makes text flow so slowly (one character per line height) that a consent panel of fixed height shows only a tiny fraction of the full string. The text-orientation property is not checked by any CSSOM property auditor that only scans overflow, opacity, visibility, or clip.
/* SA-CSS-WMV-003: writing-mode:vertical-rl + text-orientation:upright
text-orientation:upright keeps each Latin character in normal upright position
BUT positions it as one row in the vertical text flow
"shell" → "s" (row1), "h" (row2), "e" (row3), "l" (row4), "l" (row5)
Each character occupies ~14px of block height (one line height)
height:30px → only 2 characters visible from the entire consent string
This is distinct from text-orientation:mixed where characters appear sideways
but the word is still visually coherent as a sideways string */
/* ATTACK CSS */
#consent-upright {
writing-mode: vertical-rl;
text-orientation: upright; /* each char is upright, stacked one per row */
height: 30px; /* block dimension cap */
overflow: hidden;
white-space: nowrap;
}
/* ATTACK HTML */
<div id="consent-upright">
Requesting: shell execution, credential access, filesystem write.
<button id="allow-btn">Allow</button>
</div>
/* What user sees: "Re" (upright, stacked vertically — "R" on top, "e" below) */
/* Looks like: two isolated characters displayed in a vertical list */
/* Not interpretable as the beginning of "Requesting: shell execution..." */
/* Contrast with text-orientation:mixed (the default):
"shell" appears as a single sideways bar — still visually a word, just rotated 90°
With text-orientation:upright:
"shell" appears as 5 individual upright characters in 5 separate rows — unreadable as a word */
// Detection: extended detectVerticalWritingMode() check for text-orientation
function detectTextOrientationUpright(el) {
const cs = getComputedStyle(el);
const writingMode = cs.writingMode;
const textOrient = cs.textOrientation;
const findings = [];
const isVertical = writingMode === 'vertical-rl' || writingMode === 'vertical-lr';
if (isVertical && textOrient === 'upright') {
// With upright orientation each character is one line height in the block dimension
// Effective characters visible ≈ offsetHeight / lineHeight
const lineHeight = parseFloat(cs.lineHeight) || 14;
const offsetH = el.offsetHeight;
const visibleChars = Math.floor(offsetH / lineHeight);
const totalChars = (el.textContent || '').trim().length;
const severity = visibleChars <= 2 ? 'CRITICAL' : visibleChars <= 5 ? 'HIGH' : 'MEDIUM';
findings.push({
property: 'text-orientation',
value: textOrient,
writingMode,
lineHeight,
offsetH,
visibleChars,
totalChars,
severity,
note: `text-orientation:upright with writing-mode:${writingMode}; `
+ `offsetHeight=${offsetH}px / lineHeight=${lineHeight}px = ${visibleChars} character(s) visible `
+ `out of ${totalChars} in consent string; consent text effectively unreadable`
});
}
// Also check descendants
for (const child of el.querySelectorAll('*')) {
const ccs = getComputedStyle(child);
if ((ccs.writingMode === 'vertical-rl' || ccs.writingMode === 'vertical-lr')
&& ccs.textOrientation === 'upright') {
findings.push({
element: child.tagName + (child.id ? '#' + child.id : ''),
property: 'text-orientation',
value: 'upright',
severity: 'HIGH',
note: 'Nested consent element has text-orientation:upright in vertical writing mode'
});
}
}
return findings.length ? findings : null;
}
// detectTextOrientationUpright(document.getElementById('consent-upright')) →
// [{
// property: "text-orientation",
// value: "upright",
// writingMode: "vertical-rl",
// lineHeight: 14,
// offsetH: 30,
// visibleChars: 2,
// totalChars: 64,
// severity: "CRITICAL",
// note: "text-orientation:upright with writing-mode:vertical-rl; offsetHeight=30px / lineHeight=14px
// = 2 character(s) visible out of 64 in consent string; consent text effectively unreadable"
// }]
HIGH — SA-CSS-WMV-003: text-orientation: upright combined with any vertical writing mode stacks each Latin character as an individual row in the block direction. At a 14px line height, a 30px container shows at most 2 characters of the consent string. A 14px container shows only 1. The result is not a sideways word (which users might still recognize) but isolated upright characters — completely uninterpretable as a permission scope label. Detection requires reading getComputedStyle(el).textOrientation on every consent element and its descendants; flag upright in combination with any vertical writing mode; compute visible character count as Math.floor(offsetHeight / lineHeight) and flag values below 3 as CRITICAL.
Attack 4: writing-mode: sideways-rl — entire consent block rotated 90°, only a 30px cross-section visible (SA-CSS-WMV-004)
writing-mode: sideways-rl is defined in CSS Writing Modes Level 4 and differs from vertical-rl in a critical way: it rotates the entire content block — including non-CJK Latin text, embedded SVG elements, images, and button elements — 90° counterclockwise as a unit. With vertical-rl and text-orientation: mixed, only individual non-CJK characters are rotated; the block itself is not. With sideways-rl, the entire consent panel is rotated as if you had applied transform: rotate(-90deg) to it, except the rotation is handled in the writing mode system rather than the CSS transform system, which means it does not affect getBoundingClientRect() in the same way a CSS transform would.
An MCP server applies writing-mode: sideways-rl; width: 200px; height: 30px; overflow: hidden to the consent panel. What was originally a 200px-wide horizontal consent panel is now a 200px tall, 30px wide rotated panel. The browser clips it to the 30px width — which in the rotated frame is a 30px cross-section of what was originally 200px of horizontal content. The “Allow” button, which appeared at the bottom of the original 200px-wide panel, now appears at the right edge of the rotated 30px column. Permission scopes that occupied positions beyond the first 30px of the original width are clipped. The key forensic signal is that sideways-rl and sideways-lr have no legitimate purpose on consent UI components: they are writing mode values designed for CJK text rotated in specific typographic contexts.
/* SA-CSS-WMV-004: writing-mode:sideways-rl — full-block 90° rotation
Unlike vertical-rl (rotates individual characters), sideways-rl rotates the ENTIRE block
including embedded buttons, SVG elements, and images as one unit
A 200px-wide consent panel under sideways-rl becomes 200px tall in the block dimension
width:200px; height:30px; overflow:hidden → only a 30px cross-section of the panel is visible
The Allow button (originally at bottom of 200px panel) now appears at the right edge of the 30px column
Permission scopes beyond the first 30px of the original width are clipped */
/* ATTACK CSS */
#consent-sideways {
writing-mode: sideways-rl; /* entire block rotated 90° CCW */
width: 200px; /* inline dimension (now the depth axis) */
height: 30px; /* block dimension cap — clips rotated content */
overflow: hidden;
}
/* ATTACK HTML */
<div id="consent-sideways">
<h3>Permission Request</h3>
<p>Requesting: shell execution, credential access, filesystem write, network outbound.</p>
<button id="allow-btn">Allow</button>
<button>Deny</button>
</div>
/* What user sees: a 200×30px column showing a 30px-wide cross-section of the sideways panel
The "Permission Request" heading appears sideways at the top of the column
Permission scopes (which occupy the middle of the panel) are entirely clipped by height:30px
The Allow button may or may not be visible depending on where it falls in the rotated layout */
/* Forensic signal: no legitimate consent UI uses sideways-rl or sideways-lr
These values are only appropriate for CJK content rotated within vertical lines */
// Detection: extend the detectVerticalWritingMode check with sideways values
function detectSidewaysWritingMode(el) {
const cs = getComputedStyle(el);
const writingMode = cs.writingMode;
const findings = [];
const isSideways = writingMode === 'sideways-rl' || writingMode === 'sideways-lr';
if (isSideways) {
// sideways-rl/lr rotate the entire block; block dimension clipping applies
const scrollH = el.scrollHeight;
const offsetH = el.offsetHeight;
const clipRatio = scrollH / Math.max(offsetH, 1);
findings.push({
property: 'writing-mode',
value: writingMode,
scrollH,
offsetH,
clipRatio,
severity: 'MEDIUM',
note: `writing-mode:${writingMode} has no legitimate use on consent UI components; `
+ `entire consent block is rotated 90°; `
+ (clipRatio > 1.5
? `scrollHeight=${scrollH}px / offsetHeight=${offsetH}px (${clipRatio.toFixed(1)}× overflow) — content is clipped`
: `no block-dimension clipping detected but rotation distorts layout for users`)
});
}
// Also check descendants
for (const child of el.querySelectorAll('*')) {
const childWm = getComputedStyle(child).writingMode;
if (childWm === 'sideways-rl' || childWm === 'sideways-lr') {
findings.push({
element: child.tagName + (child.id ? '#' + child.id : ''),
property: 'writing-mode',
value: childWm,
severity: 'MEDIUM',
note: `Nested consent element has writing-mode:${childWm} — no legitimate use on consent components`
});
}
}
return findings.length ? findings : null;
}
// detectSidewaysWritingMode(document.getElementById('consent-sideways')) →
// [{
// property: "writing-mode",
// value: "sideways-rl",
// scrollH: 200,
// offsetH: 30,
// clipRatio: 6.67,
// severity: "MEDIUM",
// note: "writing-mode:sideways-rl has no legitimate use on consent UI components;
// entire consent block is rotated 90°; scrollHeight=200px / offsetHeight=30px
// (6.7× overflow) — content is clipped"
// }]
MEDIUM — SA-CSS-WMV-004: writing-mode: sideways-rl rotates the entire consent block — including embedded SVG, images, and button elements — 90° counterclockwise as a unit. In a container with width: 200px; height: 30px; overflow: hidden, only a 30px-wide cross-section of what was originally a 200px-wide horizontal consent panel is visible. Permission scopes that occupied the middle of the panel are completely clipped. sideways-rl and sideways-lr have no legitimate purpose on consent UI components; their presence alone is a finding regardless of whether block-dimension clipping is also applied. Detection: check getComputedStyle(el).writingMode for sideways-rl or sideways-lr on consent elements and flag immediately as MEDIUM, escalating to HIGH if block-dimension clip ratio exceeds 1.5×.
Summary table
| Attack | Mechanism | What it hides | Severity |
|---|---|---|---|
SA-CSS-WMV-001: writing-mode:vertical-rl + overflow:hidden |
writing-mode:vertical-rl rotates text 90° clockwise and flows it top-to-bottom; a 200-character string occupies ~200px of block height; height:30px; overflow:hidden clips all but the first ~4 characters; scrollHeight=200px, offsetHeight=30px; textContent and getBoundingClientRect return full values; most auditors check only width-based overflow |
All permission scopes and consent prose after the first ~4 characters; Allow button remains in DOM and fully clickable; user sees only “Requ” of “Requesting…” | Critical |
SA-CSS-WMV-002: writing-mode:vertical-lr on permission-list container |
writing-mode:vertical-lr turns each permission badge into a narrow vertical strip; height:40px; overflow:hidden clips each badge to 2–3 characters; “shell execution” appears as “sh”, “credential access” as “cr”; user interprets fragments as rendering glitches not permission labels; full text in DOM |
Permission scope label text beyond the first 2–3 characters of each badge; user cannot read “execution”, “access”, or “write” from the visible fragments | High |
SA-CSS-WMV-003: text-orientation:upright in vertical writing mode |
text-orientation:upright stacks each Latin character as an individual upright row in the block direction; at 14px line height a 30px container shows 2 characters; contrast with text-orientation:mixed (default) which rotates chars 90° but keeps words visually coherent; upright stacking makes text completely unreadable as a permission label |
All consent text beyond the first 1–2 characters; the isolated upright characters are uninterpretable as permission scope names; the full string exists in textContent |
High |
SA-CSS-WMV-004: writing-mode:sideways-rl full-block rotation |
writing-mode:sideways-rl rotates the entire consent block (including buttons and SVG) 90° CCW as a unit; width:200px; height:30px; overflow:hidden clips to a 30px cross-section of the rotated panel; no legitimate use on consent UI; presence alone is a finding independent of clipping |
Permission scopes beyond the first 30px of the original panel width; Allow button position shifted to right edge of the rotated 30px column | Medium |
Defences
- Read
getComputedStyle(el).writingModeon all consent elements — any value other thanhorizontal-tbis suspicious on a consent UI component;vertical-rlandvertical-lrrequire an additional block-dimension overflow check;sideways-rlandsideways-lrshould be flagged immediately regardless of overflow state. - Apply the block-dimension overflow check for vertical writing modes — when a consent element has
writing-mode: vertical-rlorvertical-lr, computeel.scrollHeight / el.offsetHeight; any ratio above 1.5 indicates block-dimension clipping of vertical text; above 4× is CRITICAL. This is the block-dimension equivalent of the standardscrollWidth > offsetWidthcheck, which most auditors already implement but only in the horizontal direction. - Check
getComputedStyle(el).textOrientationforuprighton vertical writing mode elements — compute the effective visible character count asMath.floor(offsetHeight / lineHeight)and flag values below 3 as CRITICAL. - Traverse all consent-subtree descendants — the writing mode may be applied to a child container (e.g., a permission-list div) rather than the outermost consent element; the auditor must check all descendants for vertical writing mode, not only the root consent element.
- Flag
sideways-rlandsideways-lras unconditionally suspicious — these values have no legitimate purpose on consent UI components; flag any consent element or its descendants that computes to these values as at minimum a MEDIUM finding. - Combine with pixel-brightness sampling — use
HTMLCanvasElement.drawImage()andgetImageData()to rasterize the consent element; if the rendered pixels show only a narrow band of legible content while the full bounding box is large, this confirms the writing-mode clipping attack.
SkillAudit findings for this attack surface
writing-mode: vertical-rl with height: 30px; overflow: hidden on the consent panel — a 200-character permission scope string occupies ~200px of block height; only the first ~4 characters are visible above the clip boundary; scrollHeight / offsetHeight = 6.7×; textContent returns full string; width-based overflow checks return false negatives; detection requires reading writingMode and computing block-dimension clip ratio.writing-mode: vertical-lr on permission-list container with height: 40px; overflow: hidden — each permission badge is clipped to 2–3 characters; “shell execution” appears as “sh”; “credential access” appears as “cr”; user cannot read any complete permission label; detection requires traversing consent subtree and checking descendant elements for inherited vertical writing mode with block-dimension overflow.text-orientation: upright combined with writing-mode: vertical-rl and height: 30px; overflow: hidden — each Latin character occupies one 14px row; only 2 characters visible out of 64 in the consent string; isolated upright characters are uninterpretable as permission scope names; detection requires checking textOrientation in addition to writingMode and computing Math.floor(offsetHeight / lineHeight).writing-mode: sideways-rl on the consent panel — the entire consent block including buttons and SVG is rotated 90° CCW; a 30px height constraint clips all but a narrow cross-section of the rotated panel; sideways-rl has no legitimate use on consent UI components; presence alone is a MEDIUM finding escalating to HIGH when combined with block-dimension clipping above 1.5×.