feat: add captcha-backed forum security hardening

This commit is contained in:
2026-03-17 16:06:28 +01:00
parent 980a15f66e
commit b3fc889452
40 changed files with 2849 additions and 108 deletions

View File

@@ -0,0 +1,95 @@
<?php
use cPad\Plugins\Forum\Services\Security\AccountFarmDetector;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Schema;
uses(Tests\TestCase::class);
it('flags repeated posting patterns across multiple accounts', function () {
config()->set('forum_bot_protection.account_farm', [
'window_minutes' => 10,
'register_attempt_threshold' => 10,
'same_ip_users_threshold' => 5,
'same_fingerprint_users_threshold' => 3,
'same_pattern_users_threshold' => 2,
'register_attempt_penalty' => 50,
'same_ip_penalty' => 35,
'same_fingerprint_penalty' => 40,
'same_pattern_penalty' => 45,
]);
Schema::dropIfExists('forum_posts');
Schema::dropIfExists('forum_bot_device_fingerprints');
Schema::dropIfExists('forum_bot_logs');
Schema::create('forum_bot_logs', function (Blueprint $table): void {
$table->id();
$table->unsignedBigInteger('user_id')->nullable();
$table->string('ip_address', 45)->nullable();
$table->string('action', 80);
$table->unsignedTinyInteger('risk_score')->default(0);
$table->string('decision', 20)->default('allow');
$table->json('metadata')->nullable();
$table->timestamp('created_at')->nullable();
});
Schema::create('forum_bot_device_fingerprints', function (Blueprint $table): void {
$table->id();
$table->unsignedBigInteger('user_id')->nullable();
$table->string('fingerprint', 128)->nullable();
$table->timestamp('first_seen')->nullable();
$table->timestamp('last_seen')->nullable();
$table->unsignedTinyInteger('risk_score')->default(0);
$table->string('user_agent_hash', 64)->nullable();
$table->timestamps();
});
Schema::create('forum_posts', function (Blueprint $table): void {
$table->id();
$table->unsignedBigInteger('thread_id')->nullable();
$table->unsignedBigInteger('topic_id')->nullable();
$table->string('source_ip_hash', 64)->nullable();
$table->unsignedBigInteger('user_id')->nullable();
$table->longText('content')->nullable();
$table->string('content_hash', 64)->nullable();
$table->boolean('is_edited')->default(false);
$table->timestamp('edited_at')->nullable();
$table->unsignedInteger('spam_score')->default(0);
$table->unsignedInteger('quality_score')->default(0);
$table->unsignedInteger('ai_spam_score')->default(0);
$table->unsignedInteger('ai_toxicity_score')->default(0);
$table->unsignedInteger('behavior_score')->default(0);
$table->unsignedInteger('link_score')->default(0);
$table->integer('learning_score')->default(0);
$table->unsignedInteger('risk_score')->default(0);
$table->integer('trust_modifier')->default(0);
$table->boolean('flagged')->default(false);
$table->string('flagged_reason')->nullable();
$table->boolean('moderation_checked')->default(false);
$table->string('moderation_status')->nullable();
$table->json('moderation_labels')->nullable();
$table->json('moderation_meta')->nullable();
$table->timestamp('last_ai_scan_at')->nullable();
$table->timestamps();
$table->softDeletes();
});
$hash = hash('sha256', 'buy cheap backlinks now');
foreach ([1, 2, 3] as $userId) {
DB::table('forum_posts')->insert([
'user_id' => $userId,
'content' => 'buy cheap backlinks now',
'content_hash' => $hash,
'created_at' => now()->subMinutes(2),
'updated_at' => now()->subMinutes(2),
]);
}
$result = app(AccountFarmDetector::class)->analyze(1, '203.0.113.10', null, 'forum_reply_create');
expect($result['score'])->toBe(45)
->and($result['reasons'])->toContain('Posting patterns or repeated content overlap across multiple accounts.');
});