Skip to content

Feature: Proof of Work captcha#4495

Open
crhallberg wants to merge 28 commits intovufind-org:devfrom
crhallberg:pow-captcha
Open

Feature: Proof of Work captcha#4495
crhallberg wants to merge 28 commits intovufind-org:devfrom
crhallberg:pow-captcha

Conversation

@crhallberg
Copy link
Contributor

@crhallberg crhallberg commented Jul 22, 2025

TODO

  • [ ] Translations (handled by Altcha)
  • [ ] Visuals (or no) (handled by Altcha)
  • Altcha support/replacement
  • Regenerate challenge when the captcha fails bc:
    • Client hits a time limit
    • Server rejects the nonce
  • [ ] Turnstyle functionality (future work)

TESTS

  • Valid
  • Invalid: bad nonce
  • Invalid: different start
  • Invalid: expired session

Projects that inspired this work

Future Work

Homegrown Solution

I started with a bespoke solution before discovering Altcha's PHP integration. I have removed my handmade version, but I have preserved it for future reference or educational reasons as a GitHub Gist.

Process

  1. (SERVER) The server generates a challenge by hashing a random number (start) and the session ID.
  2. (CLIENT) The browser receives the challenge, start, hash algo, and difficulty (based on configs).
  3. (CLIENT) Beginning with start, the browser needs to calculate a number (nonce) that, when concatenated with the challenge, generates a hash that starts with difficulty number of zeroes.
  4. (CLIENT) The nonce is added to the form data.
  5. (SERVER) The nonce is verified by regenerating the challenge from the start number and making sure the hash meets the difficulty.

Hashes

Proof of Work (PoW) is a good way to make bot traffic more expensive, slowing and discouraging crawling. While there are better hashes for PoW, I started with natively supported hashes (SHA family).

Copy link
Member

@demiankatz demiankatz left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for getting the ball rolling on this, @crhallberg. I see there's still a lot of TODO work underway, so I haven't tried it yet... but see below for some comments from an initial review. Most of what I've highlighted is comment/documentation stuff, so not exactly high priority... but might as well prevent copy-and-paste errors at the earliest opportunity! ;-)

crhallberg and others added 4 commits July 22, 2025 15:59
Co-authored-by: Demian Katz <demian.katz@villanova.edu>
Co-authored-by: Demian Katz <demian.katz@villanova.edu>
Co-authored-by: Demian Katz <demian.katz@villanova.edu>
Copy link
Member

@demiankatz demiankatz left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks, @crhallberg, I've given this another look (but still haven't tried hands-on yet). See below for some further suggestions.

…lute certainty that I forgot to update the config.ini and maybe - horror upon horrors - committed my local config (mercifully, no).
Copy link
Member

@demiankatz demiankatz left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks, @crhallberg -- see below for a few thoughts!

@crhallberg
Copy link
Contributor Author

@demiankatz in response to all the translation strings needed - consider them debug for now. Altcha does translation so the easiest solution might just be to use Altcha and remove my custom code solution. The only benefits would be file size, customization, and increasing the number of open-source projects we're responsible for.

Also, sorry for making you review a file I meant to delete (captcha-altcha.js).

Copy link
Member

@demiankatz demiankatz left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the progress, @crhallberg (and on your day off, no less). See below for a few minor new things.

/**
* Constructor
*
* @param AltchaOrg\Altcha\Altcha $altcha Required HMAC key for challenge calculation and solution verification.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This comment needs to be updated to reflect the new value (and aligned with the values below).

@crhallberg crhallberg marked this pull request as ready for review September 9, 2025 21:10
@felixlohmeier
Copy link

We had hoped that this privacy-friendly version of the proof-of-work captcha would already be included in version 11. A GDPR-compliant version with Altcha would be much more popular in Germany than an external version with Cloudflare. Thank you very much for your work on this feature so far! We eagerly await its release :).

@demiankatz
Copy link
Member

@felixlohmeier, I haven't been able to give this PR much attention because it wasn't slated for the 11.0 release, and the things that are already scheduled for that milestone have been taking up all of my time. I think we can easily aim for 11.1 with this one -- and if you have time to test and review in the meantime, getting this done in time for 11.0 is not completely impossible... but I do still have a few other things I need to give priority to first.

@demiankatz demiankatz added this to the 11.1 milestone Nov 10, 2025
Copy link
Member

@demiankatz demiankatz left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks, @crhallberg, this is off to a very good start!

See below for a couple of modernization suggestions. I've also pushed up a couple of adjustments directly (resolving conflicts, fixing outdated license comments, and preventing a fatal error if the user intentionally garbles the CAPTCHA input sent to the server).

You have quite a few to-do checkboxes here. Which of these do you consider necessary for a mergeable minimum viable product, and which do you think should be deferred until later?

Comment on lines +74 to +75
->get(\VuFind\Config\PluginManager::class)
->get('config');
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

\VuFind\Config\PluginManager has been deprecated. This should become:

Suggested change
->get(\VuFind\Config\PluginManager::class)
->get('config');
->get(\VuFind\Config\ConfigManager::class)
->getConfigArray('config');

Comment on lines +77 to +87
$secret = $config->Captcha->altcha_secret ?? null;

if (empty($secret)) {
throw new \Exception('Secret key needed for Altcha. See config.ini.');
}

$algorithm = Algorithm::from($config->Captcha->altcha_algorithm ?? 'SHA-256');
$maxNumber = $config->Captcha->altcha_max_number ?? 100000;
$saltLength = $config->Captcha->altcha_salt_len ?? 12;
$expiresInterval = $config->Captcha->altcha_expires_interval ?? null;
$params = $config->Captcha->altcha_params ?? [];
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

...and then we need to convert all the object notation to array notation:

Suggested change
$secret = $config->Captcha->altcha_secret ?? null;
if (empty($secret)) {
throw new \Exception('Secret key needed for Altcha. See config.ini.');
}
$algorithm = Algorithm::from($config->Captcha->altcha_algorithm ?? 'SHA-256');
$maxNumber = $config->Captcha->altcha_max_number ?? 100000;
$saltLength = $config->Captcha->altcha_salt_len ?? 12;
$expiresInterval = $config->Captcha->altcha_expires_interval ?? null;
$params = $config->Captcha->altcha_params ?? [];
$secret = $config['Captcha']['altcha_secret'] ?? null;
if (empty($secret)) {
throw new \Exception('Secret key needed for Altcha. See config.ini.');
}
$algorithm = Algorithm::from($config['Captcha']['altcha_algorithm'] ?? 'SHA-256');
$maxNumber = $config['Captcha']['altcha_max_number'] ?? 100000;
$saltLength = $config['Captcha']['altcha_salt_len'] ?? 12;
$expiresInterval = $config['Captcha']['altcha_expires_interval'] ?? null;
$params = $config['Captcha']['altcha_params'] ?? [];

@dmj
Copy link
Contributor

dmj commented Jan 21, 2026

Interesting. We integrated Altcha in our firewall script that we load in a modified index.php -- you can find this among other things in our SUBHH core module. We choose the "firewall approach" to avoid the heavy lifting associated with the laminas service manager.

This integrated solution has been successfully tested and is scheduled to get rolled out in the next weeks.

@demiankatz
Copy link
Member

@dmj, I think there are reasons to consider both approaches -- as a general bot blocker, your firewall approach is definitely more efficient. But this PR offers the possibility of enabling altcha to protect specific forms and functionality within the application, and that may be helpful for different use cases.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants