Guide 07 · Developer docs

Members, accounts and gated content

Building a member area on Core dna: who a member is in the data, the sign-in flows and their templates, single sign-on and passkeys, the my-account routing convention, members-only content, sessions and the page cache, and the security checks to run before launch.

L2
Unit 07 · Security for member sites

Rate limiting and captcha

Sign-in, registration and password reset are the forms that attract automated abuse. Which routes to put limits in front of, thresholds to start from, slowing down rather than blocking, and where reCAPTCHA belongs, where it is theatre, and what it owes visitors who cannot complete it.

Who this is for. Whoever plans a member site's launch, with the client and the platform team. Afterwards you know which routes need limits and a captcha, and can agree the numbers before launch rather than during an attack.

The routes to protect

Limits on these belong in front of the site, at the CDN or web application firewall that serves it, where a flood is stopped before it costs a render. Ask the platform team what the site's edge can enforce, and agree the numbers with the client.

RouteWhat an attacker wants from it
POST /login, and POST /oauth/login where OAuth sign-in is usedTo guess passwords, or test leaked ones
POST /register and POST /cdna-api/registerAccounts in bulk, for spam or to fill the client's database
POST /forgot-password and POST /cdna-api/forgot-passwordTo flood members' inboxes, and the site's email reputation with them

Thresholds to start from

There is no platform number to quote here; these are starting values to adjust once real traffic is known:

RoutePer IP addressThen
Sign-in10 attempts a minuteDelay further attempts, doubling the delay each time
Registration5 an hourDelay, and require the captcha
Forgotten password5 an hourDelay; never confirm whether the address exists

Slow down rather than block. An office, a school or a mobile network puts hundreds of people behind one address, and a block on that address locks all of them out because of one of them. A delay that grows with each attempt costs a person a few seconds and an automated attack most of its speed. Where the edge can only block, block for minutes, not days, and answer with a page that says to try again shortly.

The forgotten-password form already answers every address with the same message, "If a user account with the specified email address exists, a password reset link has been sent to it.", so a limit there protects inboxes rather than hiding accounts (Forgotten and reset passwords).

Where the captcha is available

The platform supports Google reCAPTCHA v2, as a checkbox or invisible. Its keys are part of each environment's configuration, set by the platform team, so a captcha that works on production may need keys on staging. On a member site it matters in three places:

WhereSettingDefault
Registration, both the form and the API routereCAPTCHA, in the Users registration settings: Disabled, v2 Checkbox, v2 Invisiblev2 Checkbox
Checkout, at paymentRequire captcha, in the Checkouts settings: Disabled, Enabled - Checkbox, Enabled - InvisibleEnabled - Checkbox
A form built with the Forms moduleA captcha element, and the form's Require completed captcha settingOff until added

On a Forms module form the element alone is not enough: without Require completed captcha, a submission with no captcha answer is still accepted (Captcha). There is no captcha setting for the sign-in or forgotten-password forms; the limits above are their protection.

On the registration form, render the widget only when the setting is on, inside the form, and the script after it. A failed or missing answer re-renders the form with a 422 and "Recaptcha validation failed":

Template code templates/users/register.html, inside the form
{if isset($_recaptcha)}
  {$_recaptcha->getWidgetHtml()}
  {$_recaptcha->getScriptTag()}
{/if}
{% if _recaptcha is defined %}
  {{ _recaptcha.getWidgetHtml() }}
  {{ _recaptcha.getScriptTag() }}
{% endif %}

Where a captcha belongs, and where it is theatre

A captcha belongs where an automated visitor gains something and nobody has signed in: registration, a contact or enquiry form that creates work or sends email, and checkout, where it stops card testing. It is theatre on a form behind sign-in, where the session already proves a person, and on a form whose submission does nothing. Each one costs every real visitor time, so add it where it earns that.

What it owes visitors who cannot complete it

Some people cannot complete a captcha: screen reader users meet challenges they cannot see, and people with motor or cognitive impairments struggle with timed or visual ones. A form a person cannot submit is a barrier, so every captcha-protected form needs:

  • A plain label saying what the check is and that it is needed to send the form;
  • A fallback route next to it: an email address or a phone number that reaches a person, for registration and every enquiry form;
  • Kept answers. A failed check must re-render the form with what the visitor typed, not an empty one. The registration form does this when your template prints the submitted values.

Prefer the invisible mode where it suits the form: most visitors are never challenged, and only suspicious traffic sees a puzzle.

Go deeper

Was this page helpful?

Your answer goes to the docs team.