Skip to content

Commit

Permalink
add support for 418 for dubious queries (#1875)
Browse files Browse the repository at this point in the history
* add support for 418 for dubious queries
* skip if honeypot is not enabled
  • Loading branch information
ildyria committed Jun 24, 2023
1 parent 4d889c7 commit 74bfd66
Show file tree
Hide file tree
Showing 7 changed files with 227 additions and 1 deletion.
1 change: 1 addition & 0 deletions app/Exceptions/Handler.php
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,7 @@ class Handler extends ExceptionHandler
* @var array<class-string,SeverityType>
*/
public const EXCEPTION2SEVERITY = [
HttpHoneyPotException::class => SeverityType::NOTICE, // In theory this is a 404, but because it touches honey we don't really care.
PhotoResyncedException::class => SeverityType::WARNING,
PhotoSkippedException::class => SeverityType::WARNING,
ImportCancelledException::class => SeverityType::NOTICE,
Expand Down
25 changes: 25 additions & 0 deletions app/Exceptions/HttpHoneyPotException.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
<?php

namespace App\Exceptions;

use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Exception\HttpException;

/**
* Exception thrown when attacker hits the HoneyPot.
*/
class HttpHoneyPotException extends HttpException
{
/**
* Basic constructor.
*
* @param string $path used by the attacker
* @param \Throwable|null $previous exception
*
* @return void
*/
public function __construct(string $path, \Throwable $previous = null)
{
parent::__construct(Response::HTTP_I_AM_A_TEAPOT, sprintf('The route %s could not be found.', $path), $previous);
}
}
84 changes: 84 additions & 0 deletions app/Http/Controllers/HoneyPotController.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
<?php

namespace App\Http\Controllers;

use App\Exceptions\HttpHoneyPotException;
use Illuminate\Routing\Controller;
use function Safe\preg_match;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;

/**
* This is a HoneyPot. We use this to allow Fail2Ban to stop scanning.
* The goal is pretty simple, if you are hitting this controller, and touch the honey,
* then this means that you have no interest in our pictures.
*/
class HoneyPotController extends Controller
{
public function __invoke(string $path = ''): void
{
// Check if Honey is available
if (config('honeypot.enabled', true) !== true) {
$this->throwNotFound($path);
}

/** @var array<int,string> $honeypot_paths_array */
$honeypot_paths_array = config('honeypot.paths', []);

/** @var array<int,string> $honeypot_xpaths_array_prefix */
$honeypot_xpaths_array_prefix = config('honeypot.xpaths.prefix', []);

/** @var array<int,string> $honeypot_xpaths_array_suffix */
$honeypot_xpaths_array_suffix = config('honeypot.xpaths.suffix', []);

foreach ($honeypot_xpaths_array_prefix as $prefix) {
foreach ($honeypot_xpaths_array_suffix as $suffix) {
$honeypot_paths_array[] = $prefix . '.' . $suffix;
}
}

// Turn the path array into a regex pattern.
// We escape . and / to avoid confusions with other regex characters
$honeypot_paths = '/^(' . str_replace(['.', '/'], ['\.', '\/'], implode('|', $honeypot_paths_array)) . ')/i';

// If the user tries to access a honeypot path, fail with the teapot code.
if (preg_match($honeypot_paths, $path) !== 0) {
$this->throwTeaPot($path);
}

// Otherwise just display our regular 404 page.
$this->throwNotFound($path);
}

/**
* using abort(404) does not give the info which path was called.
* This could be very useful when debugging.
* By throwing a proper exception we preserve this info.
* This will generate a log line of type ERROR.
*
* @param string $path called
*
* @return never
*
* @throws NotFoundHttpException
*/
public function throwNotFound(string $path)
{
throw new NotFoundHttpException(sprintf('The route %s could not be found.', $path));
}

/**
* Similar to abort(404), abort(418) does not give info.
* It is more interesting to raise a proper exception.
* By having a proper exception we are also able to decrease the severity to NOTICE.
*
* @param string $path called
*
* @return never
*
* @throws HttpHoneyPotException
*/
public function throwTeaPot(string $path)
{
throw new HttpHoneyPotException($path);
}
}
1 change: 0 additions & 1 deletion config/app.php
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,6 @@

'env' => env('APP_ENV', 'production'),


/*
|--------------------------------------------------------------------------
| Application Debug Mode
Expand Down
64 changes: 64 additions & 0 deletions config/honeypot.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
<?php

return [
/**
* Enable HoneyPot to return 418 when hitting honey.
*/
'enabled' => true,

/**
* Honey.
*
* Set of possible path.
* Those will be concatenated into a regex.
*/
'paths' => [
'.env',
'.git/config',
'.git/HEAD',
'.well-known/security.txt',
'.well-known/traffic-advice',

'readme.txt',
'pools',
'pools/default/buckets',
'__Additional',

'wp-login.php',
'Portal/Portal.mwsl',
'Portal0000.htm',

'aQQY',
'nmaplowercheck1686252089',
'sdk',
],

/**
* Because of all the combinations, it is more interesting to do a cross product.
*/
'xpaths' => [
'prefix' => [
'admin',
'base',
'default',
'home',
'indice',
'inicio',
'localstart',
'main',
'menu',
'start',
],

'suffix' => [
'asp',
'aspx',
'cgi',
'html',
'jhtml',
'php',
'pl',
'shtml',
],
],
];
3 changes: 3 additions & 0 deletions routes/web.php
Original file line number Diff line number Diff line change
Expand Up @@ -40,3 +40,6 @@

Route::get('/view', [IndexController::class, 'view'])->name('view')->middleware(['redirect-legacy-id']);
Route::get('/frame', [IndexController::class, 'frame'])->name('frame')->middleware(['migration:complete']);

// This route must be defined last because it is a catch all.
Route::match(['get', 'post'], '{path}', HoneyPotController::class)->where('path', '.*');
50 changes: 50 additions & 0 deletions tests/Feature/HoneyPotTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
<?php

/**
* We don't care for unhandled exceptions in tests.
* It is the nature of a test to throw an exception.
* Without this suppression we had 100+ Linter warning in this file which
* don't help anything.
*
* @noinspection PhpDocMissingThrowsInspection
* @noinspection PhpUnhandledExceptionInspection
*/

namespace Tests\Feature;

use Illuminate\Http\Response;
use Tests\AbstractTestCase;

class HoneyPotTest extends AbstractTestCase
{
public function testRoutesWithHoney(): void
{
foreach (config('honeypot.paths') as $path) {
$response = $this->get($path);
$this->assertStatus($response, Response::HTTP_I_AM_A_TEAPOT);
$response = $this->post($path);
$this->assertStatus($response, Response::HTTP_I_AM_A_TEAPOT);
}

// We check one of the version from the xpaths cross product
$response = $this->get('admin.asp');
$this->assertStatus($response, Response::HTTP_I_AM_A_TEAPOT);
}

public function testRoutesWithoutHoney(): void
{
$response = $this->get('/something');
$this->assertStatus($response, Response::HTTP_NOT_FOUND);
}

public function testDisabled(): void
{
config(['honeypot.enabled' => false]);
foreach (config('honeypot.paths') as $path) {
$response = $this->get($path);
$this->assertStatus($response, Response::HTTP_NOT_FOUND);
$response = $this->post($path);
$this->assertStatus($response, Response::HTTP_NOT_FOUND);
}
}
}

0 comments on commit 74bfd66

Please sign in to comment.