Manual:Building anti-abuse features into your extension

Anti-abuse, including but not limited to anti-spam, features are essential when it comes to extensions which allow content to be posted. "Content" should be understood here as "anything user-submitted sent to the site". If an extension fails to implement anti-abuse features, depending on how busy of a site we're talking about, it could easily become an abuse vector which in the worst case might force site admins to even disable the extension completely.

Types of anti-abuse mechanisms

  • Rate limiting (throttling)
    • Usually requires memcached or a similar cache setup
  • CAPTCHAs (the ConfirmEdit extension)
    • Different types of CAPTCHAs: distorted word (FancyCaptcha), questions (QuestyCaptcha), not-so-great JavaScript-only CAPTCHAs dependent on external services (hCaptcha etc.)
  • AbuseFilter — framework for highly customizable anti-abuse filters tailored to a specific site's particular needs
  • SpamRegex — both the core configuration variable and the extension by the same name

How do I implement...

rate limiting?

Web UI

if ( $user->pingLimiter( 'edit' ) ) {
	$out->setPageTitle( $this->msg( 'error' )->escaped() );
	$out->addWikiMsg( 'actionthrottledtext' );
	$out->addReturnTo( Title::newMainPage() ); // Make sure that there is a "use MediaWiki\Title\Title;" statement somewhere!
	return;
}

$user is a regular User object, $out is the OutputPage object.

Note that this increases the named throttle (edit). If you wish to merely check the value instead of also increasing it, use if ( $user->pingLimiter( 'edit', 0 ) ) { instead.

API

if ( $user->pingLimiter( 'edit' ) ) {
	$this->dieWithError( 'actionthrottledtext', 'throttled' );
}

Nice and simple. Read the disclaimer on the web UI section above regarding increasing throttles.

...CAPTCHAs?

This is surprisingly complex.

Web UI

For the web UI, this has been done a million times and then some. Some good examples where to borrow code:

If your extension does not have an API module or modules or you don't care about the API, that's pretty simple.

Code example from Extension:Form, for a similar SpecialPage or other ContextSource where the usual totally-not-global-variables are available:

# This part goes whereever your extension is building its HTML output:
// Is CAPTCHA enabled?
if ( $this->useCaptcha() ) {
	$out->addHTML( $this->getCaptcha() );
}
/**
 * @return bool True if CAPTCHA should be used, false otherwise
 */
private function useCaptcha() {
	global $wgCaptchaClass, $wgCaptchaTriggers;

	return $wgCaptchaClass &&
		isset( $wgCaptchaTriggers['form'] ) &&
		$wgCaptchaTriggers['form'] &&
		!$this->getUser()->isAllowed( 'skipcaptcha' );
}

/**
 * @return string CAPTCHA form HTML
 */
private function getCaptcha() {
	// NOTE: make sure we have a session. May be required for CAPTCHAs to work.
	\MediaWiki\Session\SessionManager::getGlobalSession()->persist();

	$captcha = MediaWiki\Extension\ConfirmEdit\Hooks::getInstance();
	$captcha->setTrigger( 'form' ); // <-- this is used for $wgCaptchaTriggers, see above
	$captcha->setAction( 'createpageviaform' ); // <-- you may want to change this to 'edit' or 'create'

	$formInformation = $captcha->getFormInformation();
	$formMetainfo = $formInformation;
	unset( $formMetainfo['html'] );
	$captcha->addFormInformationToOutput( $this->getOutput(), $formMetainfo );

	return '<div class="captcha">' .
		$formInformation['html'] .
		"</div>\n";
}

And this one goes for whereever you're checking the POST request:

// Check ordinary CAPTCHA
if ( $this->useCaptcha() && !MediaWiki\Extension\ConfirmEdit\Hooks::getInstance()->passCaptchaFromRequest( $request, $user ) ) {
	// Display an error message here...
	// @todo Using 'captcha-edit-fail' isn't necessarily correct, it depends on the trigger in getCaptcha()
	// The message key you use here should be consistent with the trigger name in getCaptcha()
	$out->addHTML( Html::errorBox( $this->msg( 'captcha-edit-fail' )->parse() ) ); // Again, make sure the file has a "use MediaWiki\Html\Html;" statement at the beginning of the file!
	return;
}

API

If your extension has an API module and you care about it not being abused, things get a bit hairy. You will need to have the module first get a CAPTCHA (if possible) and return it to the user for solving, and on the second try the module should process the parameters as usual.

Now, the thing is, as of June 2024 ConfirmEdit includes many frankly awful types of CAPTCHAs which rely on JavaScript and external, proprietary services. hCaptcha and reCAPTCHA are some such examples. These are simply not compatible with the API at all, period. If your API module implements ConfirmEdit support (as e.g. LinkFilter's ApiLinkEdit and ApiLinkSubmit modules do) but the wiki is using one of the aforementioned CAPTCHA types, it means that the API modules will literally be unusable by users who'd be subject to CAPTCHAs. This may or may not be a big deal to you.

...AbuseFilter support?

Web UI

TODO AFTv5 example etc.

API

TODO?

...SpamRegex support?

API and web UI

ArticleFeedbackv5 (AFTv5) does a lot of things. Its SpamRegex support is surprisingly robust. Check it out: the code is in the ArticleFeedbackv5Utils class, method validateSpamRegex.

A note about the SpamRegex extension integration in AFTv5: this code in AFTv5 checks entries which are blocked in SpamRegex in article text only, it does not check for the entries which are blocked with the "block the expression in edit summaries" option. If you wish to check for that, then either:

  1. change the line SpamRegex::TYPE_TEXTBOX to SpamRegex::TYPE_SUMMARY — this changes the code to check only for the expressions which have been blocked with the "block the expression in edit summaries" option. If you wish to check SpamRegex for both options,
  2. duplicate the relevant portion of code and then in the new, duplicated portion, change SpamRegex::TYPE_TEXTBOX to SpamRegex::TYPE_SUMMARY — that way you'll be checking for both SpamRegexed entries.

See also

Category:Security Category:Spam management
Category:Security Category:Spam management