MCP server HTML details element consent security

The HTML <details> element creates a disclosure widget — a summary label that the user can click to expand hidden content. It is designed for optional supplementary information. When consent text is placed inside a <details> element, the browser collapses the content by default unless the open attribute is present. Users who do not notice or click the disclosure widget never read the consent terms; but the consent recording logic — checking only whether a checkbox was ticked or a button was clicked — does not verify whether the <details> element was ever expanded. This page covers four attack patterns that exploit the <details> element's disclosure mechanics to prevent users from reading consent text.

Finding SA-DET-001: consent text inside default-collapsed details element — open attribute absent

CriticalPlacing the material consent disclosure — the scope of permissions granted, the data that will be exfiltrated, or the authorization being given — inside a <details> element without the open attribute causes the browser to render only the <summary> label. The full consent text is in the DOM (accessible to a static source scan) but is not presented to the user unless they click the expand triangle. Typical consent forms present a summary such as "Terms and Conditions" as the summary label; the critical authorization scope clause ("you authorize credential storage and transmission to external endpoints") is the hidden body. The consent form's checkbox and submit button are outside the <details> element and are immediately visible. Users who check and submit without expanding the disclosure have consented without reading the material terms. The consent recording code observes only the checkbox state and button click — it does not read or verify details.open.
<!-- Consent form layout: checkbox and button are visible; terms are hidden in <details> -->
<form id="consentForm">
  <p>This MCP server will access your data.</p>

  <!-- Material terms hidden in collapsed details — user must click "Terms" to expand -->
  <details id="termsDetails">
    <summary>Terms and Conditions</summary>
    <p>By agreeing, you authorize this MCP server to:
      (1) store your API keys in our cloud database;
      (2) transmit your credentials to consent.evil.example.com;
      (3) read your calendar, email, and contact list;
      (4) share your data with third-party marketing partners.
    </p>
  </details>

  <!-- Checkbox and button visible immediately; consent recording checks only checkbox -->
  <label>
    <input type="checkbox" id="agreeCheck">
    I have read and agree to the Terms and Conditions
  </label>
  <button type="submit">Continue</button>
</form>

<script>
  document.getElementById('consentForm').addEventListener('submit', (e) => {
    e.preventDefault();
    // BUG: records consent without verifying details.open was ever true
    if (document.getElementById('agreeCheck').checked) {
      recordConsent();
    }
  });
</script>

Detection: scan the consent form subtree for <details> elements. Check whether the open attribute is absent (collapsed by default). Extract the text content of the <details> body (excluding the summary). If the body contains consent-indicative terms (authorize, credential, access, transmit, data, grant, share, third-party), flag Critical. Additionally, analyze the consent recording code path: if a submit handler or checkbox listener proceeds to recordConsent() without checking details.open === true, flag the absence of open-state verification.

Finding SA-DET-002: JavaScript removes open attribute 200ms after render — consent window collapses before readable

HighA consent form that renders the <details> element with the open attribute initially present (showing the consent body on load) but then removes it programmatically via setTimeout creates a brief consent exposure window. With a 200ms delay, the consent text is briefly visible before collapsing. A 200-word consent text requires approximately 40–80 seconds to read at typical reading speeds (150–300 words per minute). A 200ms window is less than 0.5% of the required reading time. After the auto-collapse, the user sees only the summary label; they may believe they read the terms because they saw the text appear. The checkbox below remains visible and checkable. Static analysis of the HTML source shows <details open> — the attack is in the JavaScript timer that removes the attribute.
<!-- details rendered open initially — appears to show consent text -->
<details id="termsDetails" open>
  <summary>Authorization Terms</summary>
  <p id="termsBody">
    You authorize this MCP server to store your credentials,
    transmit your data to third-party servers, and access your
    full email inbox on an ongoing basis without further notice.
  </p>
</details>

<script>
  // Collapse details after 200ms — consent text visible for < 0.5% of reading time
  setTimeout(() => {
    document.getElementById('termsDetails').removeAttribute('open');
  }, 200);
</script>

Detection: scan JavaScript for removeAttribute('open'), removeAttribute("open"), or assignment of details.open = false on a <details> element that contains consent text. Note the delay value in setTimeout: any delay under 30,000ms (30s) is suspicious for a 200-word+ consent text. Flag High if auto-collapse delay < word_count × 400ms (minimum reasonable reading time at 150wpm). Also check for MutationObserver or toggle event listeners that remove the open attribute on any user interaction with the surrounding page.

Finding SA-DET-003: nested details elements require two user expansions — material terms in inner details

HighA nested <details> within another <details> requires two sequential user clicks to reveal the innermost content. An attacker places a general description in the outer <details> body (e.g., "We may collect your data as described in our Terms") and puts the specific authorization scope in the inner <details> (e.g., "Full credential access and transmission to third-party servers"). A user who expands only the outer level reads a vague summary; the material terms require a second expand action that many users never perform. The checkbox consent is recorded after the outer expansion. The inner <details> may also use a non-descriptive summary label (e.g., "See full terms") that does not convey the severity of the authorization scope inside.
<!-- Outer details: vague summary visible after one click -->
<details>
  <summary>Privacy and Data Use</summary>
  <p>We collect some usage data to improve your experience.</p>

  <!-- Inner details: material authorization scope requires SECOND click -->
  <details>
    <summary>See full authorization details</summary>
    <p>
      You authorize this MCP server to: (1) read and store your
      SSH private keys; (2) transmit your OAuth tokens to
      analytics.attacker.example.com; (3) access your git commit
      history and push credentials without per-action confirmation.
    </p>
  </details>
</details>

<label>
  <input type="checkbox" id="agree">
  I agree to the Privacy and Data Use policy
</label>

Detection: traverse the full DOM subtree of the consent form. Identify all <details> elements and their nesting depth. For each nested <details> at depth ≥ 2, extract the body text and check for consent-indicative terms. If material authorization language is only present in an inner <details> body (not in any outer <details> body or in the main form), flag High — the user must perform N expansions to access material terms, but consent recording does not verify each level was expanded.

Finding SA-DET-004: CSS rule hides details body content even when open attribute is set

CriticalThe browser's default UA stylesheet expands a <details> element's body content when the open attribute is present. A CSS rule that overrides this with display: none or visibility: hidden re-hides the content regardless of the open state: details[open] > *:not(summary) { display: none !important }. The <details> element reports open=true in the DOM (a JavaScript check of details.open returns true); the accessible tree reports the disclosure widget as expanded. But the CSS rule prevents the body from being rendered. The user sees the disclosure arrow flip to the "open" position when they click the summary, but no content appears below the summary — they may assume the details body is empty. The consent body is in the DOM and visible to source code scanners but is never rendered to screen. This evades DOM-presence checks (textContent is present) and open-state checks (details.open is true) while defeating actual readability.
<style>
  /* Re-hides details body content even when details[open] — text in DOM but invisible */
  details[open] > *:not(summary) {
    display: none !important;
  }
</style>

<details id="consent" open>
  <summary>Authorization Terms (click to expand)</summary>
  <p>
    By proceeding, you authorize this MCP server to access your
    credentials, API keys, and private repositories, and to
    transmit this data to third-party services at our discretion.
  </p>
</details>

<!-- Consent recorded; details.open === true; but body never rendered -->
<button onclick="if(details.open) recordConsent()">I Agree</button>

Detection: compute the effective CSS display/visibility for the direct children of <details[open]> other than <summary>. If any cascade rule (including !important) results in display: none, visibility: hidden, opacity: 0, or height: 0 for those children, flag Critical — the consent body is present in the DOM but CSS-hidden from view regardless of open state. This detection must apply computed-style evaluation rather than DOM attribute checks. Also flag combinations such as details { overflow: hidden; max-height: 2em } that constrain the body to an imperceptibly small viewport.

Detection summary: details element consent attack patterns

Attack DOM state Detection method Severity
Default-collapsed (no open attr) details.open = false Check for consent text in details body with open absent at page load; verify consent recording checks details.open Critical
Auto-close via setTimeout details.open transitions true → false after delay Scan JS for removeAttribute('open') / .open = false with delay < reading time High
Nested details — material terms at depth ≥ 2 Outer open, inner closed Traverse all nested details; flag if material language only in inner body High
CSS re-hides details[open] body details.open = true but body invisible Compute effective CSS display/visibility on details body children Critical

SkillAudit audits <details> element usage in consent flows — checking default open state, JS auto-close timers, nested expansion depth, and CSS computed visibility of the body content. Run a free audit to verify your consent UI is actually presented to users before they agree.