Problem/Motivation
Flag allows anonymous users to flag and unflag content. On public sites, these anonymous flag links attract a lot of automated bot traffic. The resulting flaggings are worthless and pollute counts, views and anything else built on flag data (e.g. "likes", "reports", bookmarks, voting-like features).
The recently added flood control helps against high-frequency abuse from a single source. It does not stop distributed bots or slow crawlers that simply follow every link on a page. Site builders currently have no supported way to put a human verification step in front of a flag action.
The CAPTCHA module (and its ecosystem: reCAPTCHA, hCaptcha, Friendly Captcha, Image CAPTCHA, etc.) is the established solution for this in Drupal. Flag does not currently offer an integration point for it.
Some partial workarounds exist, but none is satisfactory:
- Using the "Confirm form" link type and adding a CAPTCHA point for the confirm form's form ID. This changes the UX for all users. It does not work with the AJAX or "Normal link" types. It also relies on the form ID, which is shared across all flags, so CAPTCHA cannot be enabled per flag.
- Hiding flag links from anonymous users entirely, which defeats the purpose of anonymous flagging.
- Custom code altering links/routes, which every affected site has to reinvent.
Steps to reproduce
- Create a flag (e.g. "Like") on nodes using the AJAX or Normal link type.
- Grant the anonymous role permission to flag and unflag with it.
- Expose the site publicly and observe flaggings created by crawlers/bots following the flag links.
Proposed resolution
Reuse the CAPTCHA module's existing integration mechanism, CAPTCHA points, instead of building CAPTCHA logic into Flag. CAPTCHA points attach a challenge to a form by form ID. Flag only needs to make sure there is a suitable, per-flag form in front of the flag action. All challenge logic, configuration and exemptions (via the "Skip CAPTCHA" permission) stay in the CAPTCHA module. Flag gets no dependency on CAPTCHA, and the same approach works for other form-based anti-spam modules such as Honeypot or Antibot.
1. Flag-specific form IDs for the confirm/field entry forms
Make the form ID of the flag/unflag forms include the flag ID (e.g. flag_confirm_flag_form_like). Keep the current generic ID as base form ID via BaseFormIdInterface, so existing hook_form_FORM_ID_alter() implementations keep working. This allows a CAPTCHA point per flag and per action (flag/unflag).
2. Confirm step only when a challenge is required
Add an option to the AJAX and Normal link types: "Show confirm form (page/dialog/modal) for users who must pass a form challenge; otherwise flag directly." For the CAPTCHA case, Flag can determine this by checking whether a CAPTCHA point is enabled for the flag's form ID and whether the current user lacks the "Skip CAPTCHA" permission. Ideally this check sits behind a small event or hook, so it isn't hardcoded to CAPTCHA. This way:
- Anonymous users click the flag link and get a modal (or page) with the CAPTCHA.
- Exempt users keep the current one-click behaviour.
3. Server-side enforcement
If a flag/user combination requires the form step, the direct flag/unflag action routes must not perform the action. They should deny or redirect to the confirm form instead. Otherwise bots could bypass the challenge by calling the action URLs directly.
4. Discoverability (optional)
Show a hint in the flag settings UI (when CAPTCHA is installed) explaining how to protect the flag with a CAPTCHA point. Optionally, link directly to adding a CAPTCHA point with the flag's form ID pre-filled.
Alternatives considered
- CAPTCHA settings stored on the flag (third-party settings, CAPTCHA element added by Flag): duplicates configuration that CAPTCHA points already provide.
- A dedicated "flag challenge" API in Flag: more flexible, but reinvents what form-based integration already offers.
Remaining tasks
- Agree on the approach: direct CAPTCHA integration vs. generic challenge API (+ CAPTCHA implementation).
- Decide where the integration lives (Flag core, submodule, or CAPTCHA module).
- Implement settings UI and config schema.
- Implement behaviour for each link type, including modal handling for AJAX links.
- Enforce the check on the action link routes.
- Add tests (functional + FunctionalJavascript for the modal flow).
- Update documentation.
User interface changes
- New CAPTCHA settings on the flag edit form (only when the CAPTCHA module is installed).
- Users who require a challenge see a confirm form/modal with a CAPTCHA before flagging/unflagging. No change for users who are exempt.
API changes
- Possibly a new event/hook allowing modules to require a challenge before a flag action. Additive only, no breaking changes.
Data model changes
- New third-party settings on the flag config entity (with config schema). No changes to flagging entities.
Issue fork flag-3636853
Show commands
Start within a Git clone of the project using the version control instructions.
Or, if you do not have SSH keys set up on git.drupalcode.org:
Comments
Comment #2
anybodyComment #3
anybodyI'll prepare a proposal using AI so we can discuss and test it. We need a solution for our ongoing bot "attacks".
Comment #5
anybodyPushed an implementation of the CAPTCHA points approach to the issue fork:
3636853-provide-a-captcha-integration. No CAPTCHA dependency, and no new settings on the flag.flag_confirm_form_FLAG_ID,unflag_confirm_form_FLAG_ID,unflagging_form_FLAG_ID. The previous IDs are kept as base form IDs (BaseFormIdInterface), so existinghook_form_FORM_ID_alter()implementations keep working. A CAPTCHA point onflag_confirm_formcovers all flags; a point onflag_confirm_form_likecovers one. Field entry forms (flagging_FLAG_ID_add_form) already had per-flag IDs.flag.challengeservice deciding whether the current user must pass a challenge. It mirrors CAPTCHA's point lookup: per-flag ID, then base ID, then "enable globally", with "skip CAPTCHA" respected. Other modules can usehook_flag_challenge_required_alter().Not done here: CAPTCHA's IP whitelist is ignored (it would make links vary by IP), so whitelisted users get the confirm form without a challenge. No settings UI hint yet. That could be a follow-up.
Tests: kernel (service and CAPTCHA lookup), functional (Normal link flow, direct URL bypass, exempt users) and FunctionalJavascript (AJAX modal with failed and passed challenges, stale links, real CAPTCHA). The CAPTCHA-specific tests skip when CAPTCHA isn't available. For CI to run them,
drupal/captchaneeds to be a dev dependency. Should we add acomposer.jsonfor that?Opened a companion CAPTCHA issue for a reusable point lookup, so Flag doesn't need to mirror
captcha_form_alter(): [link]Comment #6
anybodyComment #7
anybodyWell, that looks smarter than I expected. Anyone feel free to review. I also created https://www.drupal.org/project/captcha/issues/3636870 over in the CAPTCHA module.
We'll review this more in depth now. Anyone feel free to add feedback. Especially for the case of anonymous flagging this is super helpful.