squirrelscan
GitHub

Public forms without CAPTCHA: what counts as public

A contact or newsletter form with no bot protection collects spam within days. squirrel classifies forms and flags the unprotected public ones.

What form CAPTCHA protection is

A CAPTCHA is a challenge in front of a form submission that a script cannot answer cheaply. Modern implementations are usually invisible. reCAPTCHA v3 never interrupts the visitor at all: it returns a score and your server decides what to do with a low one. Cloudflare Turnstile and hCaptcha run a background check and show a challenge only when they need one.

Any form that anyone on the internet can reach and that ends in an email or a database row is a target. Contact forms, comment boxes, newsletter signups and registration forms attract automated submissions within days of going live. A search box or a login form is a different problem: rate limiting and account lockout apply there, not a challenge on every keystroke.

What squirrel checks

The rule runs on every crawled page and emits a single check named form-captcha:

  • Info when the page contains no <form> at all. Message: No forms on page.
  • Info when no form on the page classifies as public. Message: No public forms detected.
  • Warn when at least one public form has no CAPTCHA signal. Message: 2 public form(s) without CAPTCHA, with each form identified by its id, name, action or positional index. Severity warning.
  • Pass when every public form is covered. Message: All 2 public form(s) have CAPTCHA.

A form is classified as excluded when it contains an input[type="password"] or when its action, id, class or name contains search, login, signin, sign-in, admin, checkout or cart. It is public when one of those attributes contains contact, comment, feedback, newsletter, subscribe, register, signup, sign-up, inquiry, enquiry, request, support, message or email, or when it holds a <textarea>, or an input[type="email"] with no password field. Anything else is unknown and is not checked.

Protection is detected from the provider scripts, iframes and widget classes of reCAPTCHA, Turnstile, hCaptcha and Friendly Captcha, from their response fields such as g-recaptcha-response, from preload and modulepreload hints pointing at those scripts, from Shopify’s script#captcha-bootstrap bootstrap, and from explicit-render containers whose id or class carries a provider token. A widget rendered client-side, such as @nuxtjs/turnstile or a React reCAPTCHA component, leaves only its site key in the page’s HTML, so a provider’s site key in an inline or JSON data script counts too: a nested key under the provider’s name (turnstile:{siteKey:"0x4…"}, "hcaptcha": {"sitekey": …}) or a <provider>SiteKey entry ("recaptchaSiteKey": "6L…", TURNSTILE_SITE_KEY). The value has to have that provider’s key format (Turnstile 0x4… or its 1x/2x/3x test keys, a 40-character reCAPTCHA 6L… key, an hCaptcha UUID, a Friendly Captcha FC… key), and a config that switches the provider off does not count: enabled: false, active: false or disabled: true in the provider’s own object, or a sibling flag such as recaptchaEnabled: false or TURNSTILE_ENABLED="false" beside a <provider>SiteKey. A script that only mentions a provider, or a translated “Site key” label, is not protection. FormShield is recognised from a script[src] that either carries a data-fs-project-key attribute or points at formshield.dev or a subdomain of it with a path of /js/formshield.js or /js/formshield.esm.js, and it covers every public form on the page. A page-level signal only credits a public form when that form is the only public form on the page and no other form owns a widget of its own.

How to fix it

<form action="/contact" method="post">
  <textarea name="message"></textarea>
  <div class="cf-turnstile" data-sitekey="0x4AAA..."></div>
  <button type="submit">Send</button>
</form>
<script src="https://challenges.cloudflare.com/turnstile/v0/api.js" async defer></script>

Put the widget inside the form it protects and verify the token server-side before accepting the submission. The client-side widget alone stops nothing.

A form protected by something squirrel cannot see from the HTML, such as a server-side honeypot, a rate limiter or a challenge that only mounts after user interaction, still gets flagged. Disable the rule for that case rather than adding a widget you do not need.

Rule IDsecurity/form-captcha
CategorySecurity
ScopePer-page
Severitywarning
Weight4/10

Enable / disable

Disable this rule

[rules]
disable = ["security/form-captcha"]

Disable all Security rules

[rules]
disable = ["security/*"]

Enable only this rule

[rules]
enable = ["security/form-captcha"]

enable replaces the default ["*"], so this is the only rule selected. Leave disable out: a rule that matches any disable pattern is skipped even when enable names it. Only an enabled setting under a rule’s own [rule_options] table overrides both lists.

Security findings ship in every audit next to the SEO, performance and agent experience rules. See Website security scan with AI for how an agent works through a report.

References

Check your site

Run squirrel audit https://example.com and open the Security section of the report. Every unprotected public form is listed by page with its selector. Signed-out local audits need no account and cost nothing. Signed-in audits spend credits.

Type to search…

↑↓ navigate↵ openesc close