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 patternMechanismEvasion quality
input:optional ~ .consent-disclosureFires when any input without required precedes the consent section. Most form inputs are optional by default — no injected HTML neededLooks like optional-field UX styling; :optional is underrecognized as an attack surface
textarea:optional ~ .terms-sectionTextareas (notes, comments, messages) are virtually always optional. On any form with a notes field above the consent section, fires from page loadTextareas before consent are common; the sibling rule appears incidental
select:optional ~ .consent-textDropdown selects without required are :optional. MCP adds a "preference" select (e.g., notification preference) before the consent disclosure as a plausible pre-consent optionPreference dropdowns before consent are a common UX pattern
:optional + .terms-noteAdjacent 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 elementAdjacent-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

CriticalSA-CSS-OPT-001 — 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.
HighSA-CSS-OPT-002 — 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.
HighSA-CSS-OPT-003 — 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.
HighSA-CSS-OPT-004 — :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

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.