auditmysitev2.0.2

Review CAPTCHA in authentication form

auth/captcha

WCAG success criterion
3.3.8 (level AA)
Outcome class
Hint only. An automated check that by design reports review hints only: its findings are marked for manual review, never as confirmed violations.
Area
Accessibility, Forms & Interaction
Default severity
medium
axe-core rule
accessible-auth-captcha

Why we flag this

Text and puzzle CAPTCHAs lock people with cognitive impairments, dyslexia or low vision out of signing in when no alternative is offered.

Interactive CAPTCHA widget (reCAPTCHA, hCaptcha, image CAPTCHA) in a form with a password or one-time-code field.

What the rule checks

Scope. Rendered forms with a password or one-time-code field (an authentication step).

Expectation. Needs review when such a form contains a visible CAPTCHA (reCAPTCHA or hCaptcha widget, an image whose src, alt, id or class names a captcha): it is a cognitive function test unless it is object recognition or an alternative exists, which the markup does not show. Invisible CAPTCHAs set no test and are not reported; a CAPTCHA outside authentication (a contact form) is not 3.3.8.

Source. WCAG 2.2 SC 3.3.8 Accessible Authentication (Minimum); Understanding 3.3.8 (object recognition, alternative)

Detection: shared; needs dom.

Failing example

The check flags this for review. Corpus case accessible_authentication, element form#login-captcha. The page may contain further findings for other rules.

accessible_authentication.htmlHTML
<!DOCTYPE html>
<html lang="en">
<head><meta charset="utf-8"><title>Accessible authentication</title>
<meta name="viewport" content="width=device-width, initial-scale=1"><style>
  /* #783: page shell, so the case only reports what it is about */
  .skip, nav a { display: inline-block; padding: 12px; min-height: 44px; min-width: 44px; box-sizing: border-box; }
  button, input, select, textarea { min-height: 44px; min-width: 44px; box-sizing: border-box; }
  input[type="checkbox"], input[type="radio"] { width: 44px; height: 44px; }
</style></head>
<body>
<a class="skip" href="#main">Skip to main content</a>
<header><p>Example Shop</p><nav aria-label="Main"><a href="/" aria-current="page">Home</a> <a href="/help">Help</a></nav></header>
<main id="main">
  <h1>Sign in</h1>

  <!-- Violation: paste blocked via addEventListener + preventDefault. -->
  <form id="login-blocked" action="/login" method="post">
    <label for="user-a">Username</label>
    <input id="user-a" name="username" type="text" autocomplete="username">
    <label for="pw-blocked">Password</label>
    <input id="pw-blocked" name="password" type="password" autocomplete="current-password">
    <button type="submit">Sign in</button>
  </form>

  <!-- Violation: paste blocked via inline onpaste="return false". -->
  <form id="otp-form" action="/verify" method="post">
    <label for="otp-blocked">Verification code</label>
    <input id="otp-blocked" name="otp" type="text" inputmode="numeric" autocomplete="one-time-code" onpaste="return false">
    <button type="submit">Verify</button>
  </form>

  <!-- Pass: paste allowed. -->
  <form id="login-ok" action="/login" method="post">
    <label for="user-b">E-mail</label>
    <input id="user-b" name="email" type="email" autocomplete="username">
    <label for="pw-ok">Password</label>
    <input id="pw-ok" name="password2" type="password" autocomplete="current-password">
    <button type="submit">Sign in</button>
  </form>

  <!-- Pass: handler cancels the native paste but inserts the trimmed text itself. -->
  <form id="login-sanitised" action="/login" method="post">
    <label for="pw-sanitised">Password</label>
    <input id="pw-sanitised" name="password3" type="password" autocomplete="current-password">
    <button type="submit">Sign in</button>
  </form>

  <!-- Needs review: image CAPTCHA inside an authentication form. -->
  <form id="login-captcha" action="/login" method="post">
    <label for="pw-captcha">Password</label>
    <input id="pw-captcha" name="password4" type="password" autocomplete="current-password">
    <img id="captcha-image" src="data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7" alt="captcha" width="120" height="40">
    <label for="captcha-answer">Type the characters shown</label>
    <input id="captcha-answer" name="captcha" type="text">
    <button type="submit">Sign in</button>
  </form>

  <!-- Not authentication: CAPTCHA on a contact form is out of scope. -->
  <form id="contact" action="/contact" method="post">
    <label for="msg">Message</label>
    <textarea id="msg" name="message"></textarea>
    <img id="contact-captcha" src="data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7" alt="captcha" width="120" height="40">
    <button type="submit">Send</button>
  </form>
</main>
<footer><p>Example Shop, 2026</p></footer>
<script>
  document.getElementById('pw-blocked').addEventListener('paste', function (e) {
    e.preventDefault();
    alert('Pasting is not allowed');
  });
  document.getElementById('pw-sanitised').addEventListener('paste', function (e) {
    e.preventDefault();
    var text = (e.clipboardData && e.clipboardData.getData('text/plain')) || '';
    this.value = text.trim();
  });
</script>
</body>
</html>

Passing example

The check passes here and reports nothing. Corpus case accessible_authentication, element form#contact. The page may contain further findings for other rules.

accessible_authentication.htmlHTML
<!DOCTYPE html>
<html lang="en">
<head><meta charset="utf-8"><title>Accessible authentication</title>
<meta name="viewport" content="width=device-width, initial-scale=1"><style>
  /* #783: page shell, so the case only reports what it is about */
  .skip, nav a { display: inline-block; padding: 12px; min-height: 44px; min-width: 44px; box-sizing: border-box; }
  button, input, select, textarea { min-height: 44px; min-width: 44px; box-sizing: border-box; }
  input[type="checkbox"], input[type="radio"] { width: 44px; height: 44px; }
</style></head>
<body>
<a class="skip" href="#main">Skip to main content</a>
<header><p>Example Shop</p><nav aria-label="Main"><a href="/" aria-current="page">Home</a> <a href="/help">Help</a></nav></header>
<main id="main">
  <h1>Sign in</h1>

  <!-- Violation: paste blocked via addEventListener + preventDefault. -->
  <form id="login-blocked" action="/login" method="post">
    <label for="user-a">Username</label>
    <input id="user-a" name="username" type="text" autocomplete="username">
    <label for="pw-blocked">Password</label>
    <input id="pw-blocked" name="password" type="password" autocomplete="current-password">
    <button type="submit">Sign in</button>
  </form>

  <!-- Violation: paste blocked via inline onpaste="return false". -->
  <form id="otp-form" action="/verify" method="post">
    <label for="otp-blocked">Verification code</label>
    <input id="otp-blocked" name="otp" type="text" inputmode="numeric" autocomplete="one-time-code" onpaste="return false">
    <button type="submit">Verify</button>
  </form>

  <!-- Pass: paste allowed. -->
  <form id="login-ok" action="/login" method="post">
    <label for="user-b">E-mail</label>
    <input id="user-b" name="email" type="email" autocomplete="username">
    <label for="pw-ok">Password</label>
    <input id="pw-ok" name="password2" type="password" autocomplete="current-password">
    <button type="submit">Sign in</button>
  </form>

  <!-- Pass: handler cancels the native paste but inserts the trimmed text itself. -->
  <form id="login-sanitised" action="/login" method="post">
    <label for="pw-sanitised">Password</label>
    <input id="pw-sanitised" name="password3" type="password" autocomplete="current-password">
    <button type="submit">Sign in</button>
  </form>

  <!-- Needs review: image CAPTCHA inside an authentication form. -->
  <form id="login-captcha" action="/login" method="post">
    <label for="pw-captcha">Password</label>
    <input id="pw-captcha" name="password4" type="password" autocomplete="current-password">
    <img id="captcha-image" src="data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7" alt="captcha" width="120" height="40">
    <label for="captcha-answer">Type the characters shown</label>
    <input id="captcha-answer" name="captcha" type="text">
    <button type="submit">Sign in</button>
  </form>

  <!-- Not authentication: CAPTCHA on a contact form is out of scope. -->
  <form id="contact" action="/contact" method="post">
    <label for="msg">Message</label>
    <textarea id="msg" name="message"></textarea>
    <img id="contact-captcha" src="data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7" alt="captcha" width="120" height="40">
    <button type="submit">Send</button>
  </form>
</main>
<footer><p>Example Shop, 2026</p></footer>
<script>
  document.getElementById('pw-blocked').addEventListener('paste', function (e) {
    e.preventDefault();
    alert('Pasting is not allowed');
  });
  document.getElementById('pw-sanitised').addEventListener('paste', function (e) {
    e.preventDefault();
    var text = (e.clipboardData && e.clipboardData.getData('text/plain')) || '';
    this.value = text.trim();
  });
</script>
</body>
</html>

This page is generated from the rule catalogue and the rule contracts; see therule overview for how the outcome classes are derived.