I recently built the Drupal Fence module as a way to both test out a proof of concept I had been thinking about for some time, and to get better acquainted with Drupal module development.
The concept of Drupal Fence is very simple - you define a list of routes or strings that you want to block, and Drupal Fence returns a 403 if it sees a request URI that contains a string from the list that's been defined. This list can contain known malicious URIs used by penetration testing tools such as Nikto, or even otherwise harmless URIs you don't want people accessing.
Drupal Fence integrates with Drupal's excellent flood control API to keep track of and block hosts which try to access blocked URIs too often. The flood API is extremely powerful and can keep track of hosts based on almost any data point you can think of. It can also keep track of times, thresholds, and event types, so Drupal Fence does not need to implement any of this logic at all.
Drupal Fence works by listening for a KernelEvents::REQUEST event (which fires on every request to Drupal). Then, it gets the URI and checks the user-specified blacklist for any blocked strings in the URI. If there is a match, Drupal Fence instantly returns an access denied page and registers the client with the flood control system. If a client hits the defined threshold (5 times in the span of 1 hour by default), all their requests are met with an access denied page. Drupal Fence caches all routes it has checked in the 'data' cache bin to avoid scanning URIs more than once.
This site has Drupal Fence installed with default settings, and has been configured to block the following routes containing these strings:
- 'exec//show'
- 'wp-config.php'
- '/page.cmd'
- '/shell?'