MCP server HTML input type consent security
The HTML <input> element's type attribute determines the control the browser renders, the value it submits, and the interaction model it exposes. MCP servers presenting consent checkboxes exploit four input-type attack vectors: using appearance:none with ::before pseudo-elements to fake a checked state on an unchecked checkbox, styling type=range as an indistinguishable toggle, submitting false consent via type=hidden independently of visible UI state, and replacing separate confirm buttons with type=submit to trigger form submission before the user interacts with the consent field. Each attack operates at the HTML attribute and CSS layer, below the threshold of auditors that inspect only visible DOM text.
Finding SA-INP-001: CSS appearance:none with ::before checkmark when checkbox is unchecked — fake visual state
appearance:none property removes the browser's native rendering of an <input type="checkbox">, replacing it with a blank element that the author styles entirely via CSS. When ::before or ::after pseudo-elements are used to paint the visual checkbox indicator, the state shown to the user depends entirely on which CSS selectors the author writes — not on the actual checked DOM property. An MCP server can write input[type=checkbox]:not(:checked)::before { content: "✓"; color: #22c55e; } combined with input[type=checkbox]:checked::before { content: ""; }, which inverts the visual: the user sees a green checkmark when the checkbox is not checked, and sees an empty box when they click it (checked state). The consent form can record checkbox.checked === false as implicit agreement because the user saw a checkmark and believed they had consented. Static HTML auditors that check for input[type=checkbox][checked] or inspect computed styles of the checkbox will find a structurally normal checkbox — the inversion lives entirely in the pseudo-element CSS./* ATTACK: pseudo-element inverts visual — checkmark shown when unchecked */
input[type="checkbox"].consent-check {
appearance: none;
-webkit-appearance: none;
width: 18px; height: 18px;
border: 2px solid #4ade80;
border-radius: 3px;
cursor: pointer;
}
/* Shows checkmark when NOT checked — inverted visual state */
input[type="checkbox"].consent-check:not(:checked)::before {
content: "✓";
display: block;
font-size: 14px;
color: #22c55e;
line-height: 1;
text-align: center;
}
/* Removes checkmark when clicked (checked) — user loses their "agreement" */
input[type="checkbox"].consent-check:checked::before {
content: "";
}
<!-- Consent checkbox starts in unchecked state, shows checkmark -->
<input type="checkbox" class="consent-check" id="agree" name="agree" value="1">
<label for="agree">I have read and agree to the terms of service</label>
<script>
// Records consent when checkbox.checked === false (the "checked-looking" state)
document.querySelector('#installBtn').addEventListener('click', () => {
const cb = document.getElementById('agree');
if (!cb.checked) recordConsent(); // Fires when visual shows checkmark
});
</script>
Detection: scan all <input type="checkbox"> elements used in consent flows for appearance:none or -webkit-appearance:none. When found, parse the associated CSS for ::before and ::after pseudo-element rules on that checkbox. Identify whether the pseudo-element content that represents a "checked" visual indicator (checkmark character, background-image) is applied to the :not(:checked) state rather than the :checked state — flag Critical. Also check if consent-recording JavaScript reads checkbox.checked === false or !checkbox.checked as the condition for proceeding.
Finding SA-INP-002: type=range styled as a toggle switch — always submits numeric value, user cannot "uncheck"
<input type="range"> element renders a slider control, not a checkbox. When styled with CSS to visually resemble a toggle switch or checkbox — a rounded pill shape with a circular thumb, green track on one side — users unfamiliar with HTML may interpret sliding the control to "on" as equivalent to checking a consent checkbox. The critical difference: a slider always has a numeric value (defaulting to the midpoint of its range if not specified). The MCP server can set value="100" min="0" max="100" so the slider starts fully at the "on" position with no visible thumb movement required. When the form submits, the value is always a number between 0 and 100 — the server interprets any value ≥ 50 as consent, which includes the default initial state. The user never actively agreed; the slider was pre-positioned to the consent threshold before any interaction. Unlike a checkbox, the user also cannot uncheck it — dragging to 0 just sets value=0, which the server might also interpret as consent (or simply ignore the de-consent action).<!-- ATTACK: range slider styled as toggle, pre-positioned at max -->
<style>
input[type="range"].consent-toggle {
-webkit-appearance: none;
appearance: none;
width: 52px;
height: 28px;
background: linear-gradient(to right, #22c55e 100%, #374151 0%);
border-radius: 14px;
outline: none;
cursor: pointer;
}
input[type="range"].consent-toggle::-webkit-slider-thumb {
-webkit-appearance: none;
width: 22px; height: 22px;
border-radius: 50%;
background: white;
box-shadow: 0 1px 3px rgba(0,0,0,0.3);
/* Thumb positioned at rightmost end — looks like "on" -->
}
</style>
<!-- min/max/value all at 100 — cannot be "off" unless user drags left -->
<input type="range" class="consent-toggle" name="consent_level"
min="0" max="100" value="100" id="consentToggle">
<label for="consentToggle">Allow data processing</label>
<script>
// Interprets any value >= 50 as consent — default is 100
document.querySelector('form').addEventListener('submit', e => {
if (parseInt(document.getElementById('consentToggle').value) >= 50) {
recordConsent();
}
});
</script>
Detection: identify <input type="range"> elements within consent forms or within proximity of consent text. Check CSS for -webkit-appearance:none / appearance:none plus styling that makes the range resemble a toggle (border-radius ≥ 12px on the track, circular thumb). Check the initial value attribute — if value ≥ max × 0.5, the control starts in the consent state without any user interaction required. Flag High when all three conditions are met: appearance override, toggle-like style, and initial value at or above the consent threshold.
Finding SA-INP-003: type=hidden recording false consent value independent of visible UI
<input type="hidden"> element is never displayed to the user — it exists solely to include a static or JavaScript-set value in the form submission. When an MCP server places a visible, unchecked checkbox in the consent UI as a decoy and a hidden input with name="agreed" value="1" in the same form, the form always submits agreed=1 regardless of whether the user interacted with the checkbox. The visible checkbox can have a different name (name="user_ack") that the server ignores, while the hidden input's name (agreed) is the one the backend actually checks. An auditor inspecting the DOM finds a checkbox — structurally normal. The hidden input with value="1" is the attack; it is invisible to visual inspection, does not appear in accessibility trees, and does not change state based on user interaction. A variant involves JavaScript that sets hiddenInput.value to "1" on page load before the user has interacted with anything.<form id="installForm"> <!-- Visible decoy checkbox — user sees and can interact with it --> <!-- But this field name is 'user_display_ack', ignored by server --> <input type="checkbox" name="user_display_ack" id="visibleCb"> <label for="visibleCb">I agree to the terms of service</label> <!-- ATTACK: hidden input always submits agreed=1 regardless of checkbox --> <input type="hidden" name="agreed" value="1"> <button type="submit">Install MCP Server</button> </form> <!-- Server backend only checks: if (req.body.agreed === '1') grantInstall() --> <!-- user_display_ack is never read. Form always grants install. -->
Detection: enumerate all <input type="hidden"> elements in forms that contain consent text or consent checkboxes. Cross-reference the hidden input's name attribute against what the server-side handler (or client-side consent-recording function) reads. If a hidden input with a consent-related name (agreed, consent, accepted, terms_agreed, authorized) has a static non-zero value and the visible checkbox has a different name, flag Critical. Also scan JavaScript for assignments like hiddenInput.value = "1" or hiddenInput.value = "true" that run before any user interaction event.
Finding SA-INP-004: type=submit button replacing separate confirm button — immediate form submission triggers consent recording
<button type="submit"> inside the consent form, clicking "Next Step" or "Continue" submits the form immediately — triggering the form's submit event handler, which records consent. If the checkbox is not required (required attribute absent) and the backend records consent on any form submission, the user proceeds through the workflow without explicitly acknowledging the consent field. The attack is invisible in static HTML: type="submit" is normal and expected on form buttons. The vulnerability is the combination of a non-required consent checkbox and a submit-type button whose click path does not validate checkbox state before recording.<form id="setupForm" onsubmit="handleSetup(event)">
<h2>Step 2 of 3: Permissions</h2>
<p>This server requires access to your credentials and file system.</p>
<!-- Consent checkbox — NOT marked required, user can skip -->
<input type="checkbox" name="terms" id="termsCb">
<label for="termsCb">I agree to credential access</label>
<!-- ATTACK: type=submit acts as both "Continue" button and consent recorder -->
<button type="submit">Continue to Step 3</button>
</form>
<script>
function handleSetup(e) {
e.preventDefault();
// BUG: records consent on any submit, does not check termsCb.checked
recordConsent({ agreed: true, timestamp: Date.now() });
showStep3();
}
</script>
Detection: in consent forms, identify the button that submits the form (type="submit" or type="button" with submit call). Check whether the consent checkbox carries the required attribute — if not, the form can submit without the user checking it. Trace the form's onsubmit or submit event listener: if it calls a consent-recording function without first verifying checkbox.checked === true, flag High. Also check for server-side handling — a form submission alone should not be sufficient to record consent without explicit checkbox validation.
HTML input type consent attack comparison
| Finding | Input type | Attack mechanism | Detectable by |
|---|---|---|---|
| SA-INP-001 | checkbox |
appearance:none + inverted ::before pseudo-state |
CSS pseudo-element selector analysis on consent checkboxes |
| SA-INP-002 | range |
Toggle-styled slider, default value at consent threshold | Type check + CSS appearance override + initial value ≥ 50% analysis |
| SA-INP-003 | hidden |
Static consent value submitted regardless of visible UI | Hidden input name cross-reference with consent field names |
| SA-INP-004 | submit |
Form submission records consent without checkbox validation | Submit handler analysis + missing required attribute check |
SkillAudit inspects all <input> elements within MCP server consent flows — checking type attribute, appearance CSS, pseudo-element state logic, hidden field values, and submit handler consent validation. Run a free audit to verify your consent checkboxes accurately reflect user intent before recording agreement.