MCP server CSS ::placeholder security: consent disclosure hidden in input placeholder, color transparency, font-size collapse, text-overflow ellipsis clipping, and opacity attack
Published 2026-07-25 — SkillAudit Research
The CSS ::placeholder pseudo-element controls the visual presentation of an HTML input's placeholder attribute text. In a standard consent flow, consent text appears in a paragraph or label element, making ::placeholder irrelevant. But some MCP server consent implementations — particularly those that integrate consent acknowledgment directly into sign-up forms — place a consent disclosure as the placeholder attribute of a text input or email field. The user is expected to see and read the consent disclosure in the placeholder before typing their email address. Once the user types a character, the placeholder disappears and consent is deemed given.
This pattern is already a GDPR Art. 7 compliance failure on its own (consent in a field that disappears on interaction is not "freely given, specific, informed"). But MCP servers can make it worse: the ::placeholder pseudo-element allows MCP to make the placeholder consent text invisible, collapsed, or truncated before the user ever types — so the disclosure never appears at all, and the consent record shows a signed-in user who "saw the consent in the placeholder."
Placeholder consent is already non-compliant: Consent disclosure placed in an input placeholder violates GDPR Art. 7(2) requirements for clear presentation and WCAG 2.1 SC 3.3.2 (labels or instructions) because placeholder text disappears on focus/input. The ::placeholder CSS attacks described here compound this existing non-compliance by additionally hiding the already-inadequate disclosure.
Attack 1: color:transparent hides placeholder consent text entirely
An MCP server places the consent disclosure in the placeholder attribute of the sign-up input field: <input type="email" placeholder="By entering your email you agree to data processing per GDPR Art.6(1)(a). Enter email here.">. The MCP then applies ::placeholder { color: transparent }. The placeholder text is still present in the DOM (in the placeholder attribute), is still read by screen readers (via aria-placeholder or UA behavior), and is still visible to DOM-based consent audits. But visually, the placeholder text renders as transparent characters on a white background — completely invisible to sighted users. The input appears to be an empty, unlabeled email field.
/* MCP attack — placeholder consent made invisible: */
#email-signup-input::placeholder,
.consent-input::placeholder,
input[name="email"]::placeholder {
color: transparent;
/* Placeholder text: "By entering your email you agree to
processing of your personal data under GDPR Art.6(1)(a).
We share data with advertising partners. See privacy policy."
All of this text is in the DOM (input.placeholder attribute).
Screen reader: announces placeholder text (UA-dependent).
Sighted user: sees an empty email input field — no disclosure visible.
Standard audit:
document.querySelector('#email-signup-input').placeholder → full text (present)
getComputedStyle(input).color → '#1a1a1a' (the INPUT color, not placeholder)
getComputedStyle(input, '::placeholder').color → 'rgba(0,0,0,0)' (the attack)
*/
}
// Detection: must query ::placeholder computed style explicitly
function detectPlaceholderColorAttack() {
const inputs = document.querySelectorAll(
'input[placeholder], textarea[placeholder]'
);
inputs.forEach(el => {
const placeholder = el.getAttribute('placeholder') || '';
// Only check inputs where placeholder contains consent-related text
const consentKeywords = [
'agree', 'consent', 'terms', 'privacy', 'gdpr', 'data', 'processing',
'accept', 'by entering', 'by clicking', 'by signing'
];
const hasConsentText = consentKeywords.some(kw =>
placeholder.toLowerCase().includes(kw)
);
if (hasConsentText) {
const pseudoStyle = window.getComputedStyle(el, '::placeholder');
const pseudoColor = pseudoStyle.color;
const pseudoOpacity = pseudoStyle.opacity;
if (pseudoColor === 'rgba(0, 0, 0, 0)' || pseudoColor === 'transparent' ||
pseudoOpacity === '0') {
console.error('SECURITY: ::placeholder consent text is invisible:', {
el, placeholder: placeholder.substring(0, 100),
pseudoColor, pseudoOpacity
});
}
// Check for background-color matching the input background (camouflage)
const inputBg = window.getComputedStyle(el).backgroundColor;
const pseudoBg = pseudoStyle.backgroundColor;
if (pseudoBg !== 'rgba(0, 0, 0, 0)' && pseudoBg === inputBg) {
console.error('SECURITY: ::placeholder background matches input background (camouflage):', {
el, inputBg, pseudoBg
});
}
}
});
}
Attack 2: font-size:0 collapses placeholder consent text to zero height
Setting ::placeholder { font-size: 0 } on a consent-bearing input collapses the placeholder text to zero visible size. The placeholder text is still present in the DOM attribute and technically occupies the input's content area — but glyphs at zero font-size render as zero-height, zero-advance-width invisible characters. Unlike the color-transparency attack, this approach also prevents screen readers from reading out the placeholder text in some UA implementations (since zero-font-size may cause the AT to treat the content as empty). The input appears completely unlabeled — the email field has no visible placeholder and no visible label, making the consent disclosure invisible in two modes simultaneously.
/* MCP attack — placeholder consent collapsed to zero size: */
.consent-form input::placeholder {
font-size: 0;
/* Zero font-size collapses all glyph rendering.
Computed font-size of the ::placeholder pseudo-element: 0px.
The input appears to have no placeholder text.
Screen reader behavior is UA-dependent: some ATs read the placeholder
attribute regardless of CSS font-size; others suppress zero-size text.
Either way, the visual consent disclosure is completely absent. */
}
/* Variant: sub-pixel font-size (technically non-zero but imperceptible): */
.consent-form input::placeholder {
font-size: 0.01px;
/* Glyphs are 0.01px tall — invisible to human vision but technically present.
May trip simpler font-size=0 checks. Computed value: 0.01px */
}
// Detection: check ::placeholder font-size
function detectPlaceholderFontSizeCollapse() {
for (const sheet of document.styleSheets) {
try {
for (const rule of sheet.cssRules) {
if (rule instanceof CSSStyleRule &&
rule.selectorText && rule.selectorText.includes('::placeholder')) {
const fontSize = rule.style.fontSize;
if (fontSize) {
const parsed = parseFloat(fontSize);
if (parsed === 0 || (parsed > 0 && parsed < 1)) {
console.error('SECURITY: ::placeholder font-size collapses consent text:', {
selector: rule.selectorText, fontSize
});
}
}
}
}
} catch (e) { /* cross-origin */ }
}
// Also check computed values on actual consent inputs
const inputs = document.querySelectorAll('input[placeholder]');
inputs.forEach(el => {
const pseudoStyle = window.getComputedStyle(el, '::placeholder');
const pseudoFontSize = parseFloat(pseudoStyle.fontSize);
const elFontSize = parseFloat(window.getComputedStyle(el).fontSize);
if (pseudoFontSize < elFontSize * 0.1) {
const ph = el.getAttribute('placeholder') || '';
const consentKeywords = ['agree', 'consent', 'terms', 'privacy', 'data'];
if (consentKeywords.some(kw => ph.toLowerCase().includes(kw))) {
console.error('SECURITY: ::placeholder font-size is', pseudoFontSize + 'px',
'on consent-bearing input (element font-size:', elFontSize + 'px):', { el });
}
}
});
}
Attack 3: text-overflow:ellipsis clips long consent placeholder to "..."
The ::placeholder pseudo-element supports text-overflow. An MCP server places a full consent disclosure in the placeholder — "By entering your email address you consent to: storage and processing of your personal data by SkillAudit and its partners; transfer of data to the United States under Standard Contractual Clauses; and use of your data for marketing communications" — but applies text-overflow: ellipsis; white-space: nowrap; overflow: hidden to the ::placeholder. On a standard-width input field, "By entering your email address you consent to: s…" becomes "By entering…" or simply "By…" depending on the input width. The user sees only the opening words, assumes the rest is boilerplate, and proceeds. The full legal consent text is in the DOM but never visually presented.
/* MCP attack — long consent placeholder clipped to ellipsis: */
.signup-field::placeholder {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
/* Placeholder: "By entering your email you consent to data processing,
transfer to US partners under SCCs, and use for marketing."
Rendered in a 300px input at 14px font: "By entering your email you..." or less
The critical legal content (data sharing, US transfer, marketing) is clipped.
This attack is particularly dangerous because:
- text-overflow:ellipsis is a legitimate UI pattern (not suspicious)
- The DOM contains the full consent text (audit passes)
- The rendered output looks like a standard "Enter your email" hint
- The user never sees anything beyond the opening clause */
}
// Detection: check for text-overflow:ellipsis on ::placeholder for consent inputs
function detectPlaceholderEllipsisClip() {
const inputs = document.querySelectorAll('input[placeholder], textarea[placeholder]');
inputs.forEach(el => {
const placeholder = el.getAttribute('placeholder') || '';
const consentKeywords = ['agree', 'consent', 'terms', 'privacy', 'gdpr', 'data processing'];
const hasConsentText = consentKeywords.some(kw => placeholder.toLowerCase().includes(kw));
if (!hasConsentText) return;
const pseudoStyle = window.getComputedStyle(el, '::placeholder');
const textOverflow = pseudoStyle.textOverflow || pseudoStyle.getPropertyValue('text-overflow');
const whiteSpace = pseudoStyle.whiteSpace;
const overflow = pseudoStyle.overflow;
if (textOverflow === 'ellipsis' || (whiteSpace === 'nowrap' && overflow === 'hidden')) {
console.error('SECURITY: consent placeholder uses text-overflow clipping:', {
el,
placeholder: placeholder.substring(0, 80) + '...',
textOverflow, whiteSpace, overflow,
inputWidth: el.offsetWidth + 'px'
});
// Measure how much of the placeholder is actually visible
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d');
const styles = window.getComputedStyle(el);
ctx.font = `${pseudoStyle.fontSize} ${pseudoStyle.fontFamily || styles.fontFamily}`;
const fullWidth = ctx.measureText(placeholder).width;
const inputWidth = el.clientWidth;
if (fullWidth > inputWidth) {
const visibleChars = Math.floor(placeholder.length * (inputWidth / fullWidth));
console.error('SECURITY: consent placeholder visible portion:', {
visibleText: placeholder.substring(0, visibleChars) + '...',
fullLength: placeholder.length,
visibleLength: visibleChars,
hiddenText: placeholder.substring(visibleChars)
});
}
}
});
}
Attack 4: opacity:0 on ::placeholder makes consent disclosure invisible while maintaining DOM presence
Unlike color: transparent which affects only the text color, opacity: 0 on ::placeholder makes the entire pseudo-element invisible including background colors, borders, and decorations. The placeholder text is in the DOM, the computed opacity of the input element itself is 1 (fully opaque), but the ::placeholder computed opacity is 0. This attack is more comprehensive than the color-transparency variant: even if an auditor checks for text-decoration: underline or background-color on the placeholder (hoping to detect text even if the color is transparent), opacity:0 suppresses all rendering of the pseudo-element. An input with a 200-character consent disclosure in its placeholder attribute appears completely empty.
/* MCP attack — ::placeholder made fully invisible via opacity: */
.consent-gate-input::placeholder {
opacity: 0;
/* Completely transparent — all rendering of the placeholder
pseudo-element is suppressed, including text-decoration,
background, and outline.
Contrast with color:transparent:
- color:transparent: text color transparent but underline/decoration survives
- opacity:0: everything in the pseudo-element is invisible
DOM audit:
el.placeholder → full consent text (present)
getComputedStyle(el).opacity → '1' (the input is visible)
getComputedStyle(el, '::placeholder').opacity → '0' (the attack)
*/
}
/* Combined attack for maximum evasion: */
.consent-gate-input::placeholder {
opacity: 0;
font-size: 0.01px;
color: transparent;
/* Triple redundancy: opacity:0 makes it invisible; font-size:0.01px collapses
it spatially; color:transparent makes glyphs transparent.
Even if one technique is detected/blocked, the others still apply. */
}
// Detection: audit ::placeholder opacity
function detectPlaceholderOpacity() {
const inputs = document.querySelectorAll('input[placeholder], textarea[placeholder]');
inputs.forEach(el => {
const pseudoStyle = window.getComputedStyle(el, '::placeholder');
const opacity = parseFloat(pseudoStyle.opacity);
if (opacity < 0.2) { // <20% opacity on placeholder is suspicious
const ph = el.getAttribute('placeholder') || '';
console.error('SECURITY: ::placeholder has low opacity (' + opacity + ') on input:', {
el, opacity, placeholderPreview: ph.substring(0, 80)
});
}
});
// Scan stylesheets for ::placeholder opacity rules
for (const sheet of document.styleSheets) {
try {
for (const rule of sheet.cssRules) {
if (rule instanceof CSSStyleRule &&
rule.selectorText && rule.selectorText.includes('::placeholder')) {
const opacity = rule.style.opacity;
if (opacity !== '' && parseFloat(opacity) < 0.2) {
console.error('SECURITY: ::placeholder opacity attack in stylesheet:', {
selector: rule.selectorText, opacity
});
}
}
}
} catch (e) { /* cross-origin */ }
}
}
// Comprehensive ::placeholder consent audit — run all four checks
function auditPlaceholderConsent() {
detectPlaceholderColorAttack();
detectPlaceholderFontSizeCollapse();
detectPlaceholderEllipsisClip();
detectPlaceholderOpacity();
// Bonus: check if any input with long placeholder has no visible label element
document.querySelectorAll('input[placeholder]').forEach(el => {
const ph = el.getAttribute('placeholder') || '';
const consentKeywords = ['agree', 'consent', 'terms', 'privacy', 'gdpr'];
if (consentKeywords.some(kw => ph.toLowerCase().includes(kw)) && ph.length > 50) {
// Consent text in placeholder (any length > 50 chars with consent keywords)
const label = document.querySelector(`label[for="${el.id}"]`) ||
el.closest('label');
if (!label) {
console.warn('SECURITY: consent text in placeholder attribute with no visible label:', {
el, placeholder: ph.substring(0, 100)
});
}
}
});
}
Consent-in-placeholder is doubly non-compliant: GDPR Art. 7(2) requires consent requests to be "clearly distinguishable from other matters" and "presented in a clear and plain manner." A consent disclosure that disappears when the user clicks the field (standard placeholder behavior) does not meet this bar. MCP server ::placeholder CSS attacks add a second layer of non-compliance by also making the already-inadequate placeholder invisible. Automated audits that read input.placeholder from the DOM will incorrectly report consent as present — a false negative that requires pseudo-element style inspection to detect.
Attack summary
| Attack | CSS property | Effect on consent | Severity |
|---|---|---|---|
| Color transparency | ::placeholder { color: transparent } |
Placeholder consent text renders as transparent glyphs | High |
| Font-size collapse | ::placeholder { font-size: 0 } |
Consent text glyphs rendered at zero height — invisible | High |
| Text-overflow ellipsis | ::placeholder { text-overflow: ellipsis; white-space: nowrap } |
Long consent text clipped to opening words + "..." | High |
| Opacity zero | ::placeholder { opacity: 0 } |
Entire pseudo-element invisible; suppresses all visual rendering | High |
Consolidated findings
placeholder attribute and applies text-overflow: ellipsis; white-space: nowrap; overflow: hidden to ::placeholder. On a standard 300px input at 14px, the visible portion is limited to the first ~40 characters. Critical terms (data transfer, marketing use, third-party sharing) are clipped. DOM-based audits that check input.placeholder find the full text present. Only inspection of the pseudo-element's text-overflow style and a Canvas measurement of rendered width reveals the truncation.
opacity: 0 or color: transparent to ::placeholder on the email/signup input, making the consent disclosure in the placeholder attribute invisible to sighted users. The DOM contains the full consent text, causing DOM-based audits to incorrectly report consent as present. Detection requires getComputedStyle(input, '::placeholder').opacity and .color checks — not element-level computed style queries.
← Blog | CSS ::first-letter attacks | CSS ::marker attacks | Security Checklist