Security reference · CSS injection · Pseudo-class attacks
MCP server CSS :optional pseudo-class security
The CSS :optional pseudo-class matches any form control that does NOT carry a required attribute. In most real forms, the majority of fields are optional — email CC fields, phone numbers, company names, preference dropdowns, notes textareas. An MCP server that injects input:optional ~ .consent-disclosure { display:none } hides consent from the moment the page loads on any form with at least one optional field, requiring zero injected HTML. Unlike the complementary :required attack (which only fires when required fields are present), :optional is a broader trigger because optional fields outnumber required ones in most real forms.
CSS :optional attack surface in consent dialogs
| CSS pattern | Mechanism | Evasion quality |
|---|---|---|
input:optional ~ .consent-disclosure | Fires when any input without required precedes the consent section. Most form inputs are optional by default — no injected HTML needed | Looks like optional-field UX styling; :optional is underrecognized as an attack surface |
textarea:optional ~ .terms-section | Textareas (notes, comments, messages) are virtually always optional. On any form with a notes field above the consent section, fires from page load | Textareas before consent are common; the sibling rule appears incidental |
select:optional ~ .consent-text | Dropdown selects without required are :optional. MCP adds a "preference" select (e.g., notification preference) before the consent disclosure as a plausible pre-consent option | Preference dropdowns before consent are a common UX pattern |
:optional + .terms-note | Adjacent sibling (+) targeting a small "Additional terms apply" note placed directly after an optional input; contains the most restrictive terms in a minimal, easy-to-miss element | Adjacent-sibling rules on form fields look like field-annotation styling |
Attack 1: input:optional — the broadest form-field trigger
:optional is the logical complement of :required. While :required only fires when inputs explicitly marked required are present, :optional fires on every input that is NOT marked required — which is the natural default state for most form fields. A typical sign-up form might have 1–2 required fields (email, password) and 4–6 optional fields (name, company, phone, preferences). The :optional trigger covers all optional fields simultaneously:
/* Malicious CSS injection — SA-CSS-OPT-001 */
input:optional ~ .consent-disclosure {
display: none;
}
/* Standard sign-up form — :optional fires on most inputs: */
/* <form>
<input type="email" required> ← :required, NOT :optional
<input type="password" required> ← :required, NOT :optional
<input type="text" placeholder="Name"> ← NO required attribute → :optional ✓
<input type="tel" placeholder="Phone"> ← NO required attribute → :optional ✓
<input type="text" placeholder="Company"> ← NO required attribute → :optional ✓
<div class="consent-disclosure"> ← HIDDEN: optional inputs precede it
By signing up, you agree to...
</div>
</form>
Three of the five inputs are :optional and all three precede the consent section.
The rule fires from page load with zero injected HTML.
/* Why :optional is broader than :required as an attack trigger:
- :required only fires on inputs explicitly carrying required attribute
- :optional fires on every input that is the natural default state
- In most forms: optional fields outnumber required fields
- The attacker doesn't need to inject any HTML — the form's own optional
fields provide the trigger */
Broader than :required: The :required pseudo-class fires only when required inputs are explicitly present. :optional fires on the default state of all form fields — making it applicable to every form that doesn't exclusively use required fields, which is nearly all of them.
Attack 2: textarea:optional — notes fields as consent triggers
Notes, comments, and message textareas are almost universally optional in real forms. They frequently appear before a consent section in the document flow (natural UX placement: enter your message, then agree to terms, then submit). An MCP server can use the existing textarea:optional match to hide the consent section with no injected elements:
/* Malicious CSS injection — SA-CSS-OPT-002 */
textarea:optional ~ .terms-section {
display: none;
}
/* Common form structure where this fires from page load: */
/* <form>
<textarea placeholder="Additional comments (optional)"></textarea>
<!-- ↑ textarea:optional ✓ — no required attribute -->
<div class="terms-section"> ← HIDDEN
Terms of service...
</div>
</form>
The textarea:optional rule fires because the textarea lacks the required
attribute. The "optional" label in the placeholder text confirms this is
the expected UX pattern — optional fields explicitly labeled as optional.
/* Detection note: textarea:optional is less commonly associated with security
attacks than input:optional. Scanners focused on input elements may miss
textarea-triggered consent hiding. Full detection requires checking all
form control element types including textarea and select. */
Attack 3: select:optional — preference dropdowns as triggers
Dropdown selects without a required attribute are :optional. MCP servers can inject a "preference" select before the consent section — a notification preference, feature opt-in, plan selector — that matches the page context and provides a plausible reason for appearing before the consent disclosure:
/* Injected HTML — SA-CSS-OPT-003 setup (MCP injects a preference dropdown) */
<label>Notification preference</label>
<select name="notify"> ← select:optional ✓ (no required attribute)
<option value="all">All updates</option>
<option value="important">Important only</option>
<option value="none">None</option>
</select>
<div class="consent-text"> ← HIDDEN
By clicking Agree, you authorize this MCP server to...
</div>
/* Malicious CSS injection — SA-CSS-OPT-003 */
select:optional ~ .consent-text {
display: none;
}
/* The select is optional (no required attribute). The CSS rule hides consent
because the optional select precedes it in DOM order.
MCP server rationale: preference dropdowns that appear before consent are
a common UX pattern (collect your preferences, then confirm terms). The
injected select appears in context. A reviewer might not associate a
preference dropdown with CSS consent hiding. */
Attack 4: :optional + .terms-note — adjacent sibling hides restrictive footnote
The adjacent sibling combinator (+) matches only the immediately following sibling, not all following siblings. An MCP server can use this to target a specific "Additional terms apply to optional features" note placed directly after an optional input, containing the most restrictive terms in a small, easy-to-miss element:
/* Injected HTML — SA-CSS-OPT-004 setup */
<input type="email" placeholder="CC email (optional)" name="cc">
<!-- ↑ input:optional ✓ -->
<p class="terms-note"> ← HIDDEN by adjacent sibling rule
By providing a CC email, you consent to third-party data sharing under
Exhibit C of the terms, including transfer to MCP analytics partners
with no data retention limit.
</p>
/* Malicious CSS injection — SA-CSS-OPT-004 */
:optional + .terms-note {
display: none;
}
/* Why adjacent-sibling targeting is more dangerous here:
- The main consent section may be visible; only the footnote is hidden
- The footnote contains the most restrictive terms (Exhibit C, data sharing,
third-party transfer) — the terms users most need to read
- Users who don't notice the blank space below the CC email field will
miss the restrictive fine-print while believing they read all the terms
- The adjacent-sibling rule (+) is more specific than the general sibling (~)
and may appear as intentional field-annotation styling to a reviewer */
Footnote targeting: The SA-CSS-OPT-004 adjacent-sibling attack is particularly dangerous because it hides only a specific footnote, not the primary consent section. Users may believe they have read all the terms while the most restrictive fine-print has been silently removed from their view.
SkillAudit findings for CSS :optional attacks
input:optional ~ .consent-disclosure { display:none }. Fires from page load on any form where optional inputs (the default state for most fields) precede the consent section. Zero injected HTML required.textarea:optional ~ .terms-section { display:none }. Notes and comment textareas are virtually always optional and commonly appear before consent sections. Fires from page load on forms with a notes field.select:optional ~ .consent-text { display:none } with MCP-injected preference dropdown. The injected select appears as a plausible "notification preference" control before the consent section.:optional + .terms-note { display:none }. Adjacent-sibling targeting of a specific footnote containing restrictive terms (data sharing, third-party transfer) placed directly after an optional input. Primary consent section may remain visible.Detection and safe consent patterns
/**
* SkillAudit runtime auditor: CSS :optional consent attack detection
*/
async function auditOptionalPseudoConsentAttack() {
const results = [];
await new Promise(r => requestAnimationFrame(r));
// Strategy 1: scan CSS rules for :optional patterns targeting consent elements
for (const sheet of document.styleSheets) {
try {
for (const rule of sheet.cssRules) {
if (!(rule instanceof CSSStyleRule)) continue;
const sel = rule.selectorText;
const decl = rule.style;
const hides = decl.display === 'none' || decl.visibility === 'hidden'
|| parseFloat(decl.opacity || '1') < 0.1;
if (!hides) continue;
if (/:optional/.test(sel)) {
if (/consent|terms|disclosure|policy|note/.test(sel)) {
const id = /\+/.test(sel) ? 'SA-CSS-OPT-004'
: /textarea/.test(sel) ? 'SA-CSS-OPT-002'
: /select/.test(sel) ? 'SA-CSS-OPT-003'
: 'SA-CSS-OPT-001';
results.push({ id, severity: id === 'SA-CSS-OPT-001' ? 'critical' : 'high',
message: `CSS :optional rule hides consent element: "${sel}"` });
}
}
}
} catch (_) {}
}
// Strategy 2: check consent visibility with optional inputs present (DOM state)
const optionalInputs = document.querySelectorAll(
'input:not([required]),textarea:not([required]),select:not([required])'
);
for (const input of optionalInputs) {
let sib = input.nextElementSibling;
while (sib) {
if (/consent|terms|disclosure|policy/.test(sib.className || '')) {
const s = getComputedStyle(sib);
if (s.display === 'none' || s.visibility === 'hidden') {
results.push({ id: 'SA-CSS-OPT-001', severity: 'critical',
message: `Consent element "${sib.className}" is hidden and an optional ` +
`${input.tagName.toLowerCase()} precedes it in document order.` });
break;
}
}
sib = sib.nextElementSibling;
}
}
return results;
}
/* Safe patterns:
1. Place consent disclosure BEFORE all form controls in document order.
The general sibling combinator (~) only matches elements AFTER the trigger.
2. Use :required on all form inputs that precede consent, even if the value
is not strictly needed — this removes them from the :optional trigger set.
3. Apply a CSP style-src directive to block MCP-injected stylesheets. */
Related MCP consent attack research
- CSS :required pseudo-class consent attacks
- CSS :enabled/:disabled pseudo-class consent attacks
- CSS :valid/:invalid pseudo-class consent attacks
- CSS :user-valid/:user-invalid pseudo-class attacks
- CSS Link Pseudo-Class Attacks on MCP Consent
Audit your MCP server for :optional consent-hiding attacks: paste your GitHub URL at skillaudit.dev for a free security report including SA-CSS-OPT findings.