mirror of
https://github.com/HolgerHatGarKeineNode/einundzwanzig-nostr.git
synced 2026-06-04 02:05:35 +00:00
5f28bfedd4
- **SecurityMonitor:** Added logic to record and prevent logging of locked-property exceptions, while ensuring non-security exceptions are properly forwarded. - **Livewire `Members/Admin`:** Centralized authorization logic in private methods, enforced access control on actions, and moved allowed pubkeys to class constant for maintainability. - **Livewire `News`:** Enforced authorization for editing and deleting news with guard methods and ensured unauthorized users can't access data. - **Bootstrap exceptions:** Implemented custom exception handling to record Livewire-related security issues while preventing redundant logs. - Updated tests with new behavior verification covering access control and exception responses.
201 lines
6.3 KiB
PHP
201 lines
6.3 KiB
PHP
<?php
|
|
|
|
use App\Models\SecurityAttempt;
|
|
use App\Services\SecurityMonitor;
|
|
use Illuminate\Contracts\Debug\ExceptionHandler;
|
|
use Illuminate\Http\Request;
|
|
use Illuminate\Support\Facades\Log;
|
|
use Livewire\Features\SupportLockedProperties\CannotUpdateLockedPropertyException;
|
|
|
|
beforeEach(function () {
|
|
$this->monitor = app(SecurityMonitor::class);
|
|
});
|
|
|
|
it('records locked property exceptions', function () {
|
|
$exception = new CannotUpdateLockedPropertyException('isLoggedIn');
|
|
|
|
$request = Request::create('/livewire/update', 'POST', [
|
|
'components' => [
|
|
[
|
|
'snapshot' => json_encode([
|
|
'memo' => ['name' => 'auth-button'],
|
|
]),
|
|
'updates' => ['isLoggedIn' => true],
|
|
],
|
|
],
|
|
]);
|
|
|
|
$request->setRouteResolver(fn () => null);
|
|
|
|
app()->instance('request', $request);
|
|
|
|
$this->monitor->recordFromException($exception, $request);
|
|
|
|
expect(SecurityAttempt::count())->toBe(1);
|
|
|
|
$attempt = SecurityAttempt::first();
|
|
expect($attempt->exception_class)->toBe(CannotUpdateLockedPropertyException::class)
|
|
->and($attempt->target_property)->toBe('isLoggedIn')
|
|
->and($attempt->component_name)->toBe('auth-button')
|
|
->and($attempt->severity)->toBe('medium');
|
|
});
|
|
|
|
it('detects high severity injection attempts', function () {
|
|
$exception = new CannotUpdateLockedPropertyException('isLoggedIn');
|
|
|
|
$request = Request::create('/livewire/update', 'POST', [
|
|
'components' => [
|
|
[
|
|
'snapshot' => json_encode([
|
|
'memo' => ['name' => 'auth-button'],
|
|
]),
|
|
'updates' => [
|
|
'isLoggedIn' => [
|
|
'__toString' => 'phpinfo',
|
|
'SerializableClosure' => [],
|
|
],
|
|
],
|
|
],
|
|
],
|
|
]);
|
|
|
|
$request->setRouteResolver(fn () => null);
|
|
|
|
$this->monitor->recordFromException($exception, $request);
|
|
|
|
$attempt = SecurityAttempt::first();
|
|
expect($attempt->severity)->toBe('high');
|
|
});
|
|
|
|
it('does not record non-security exceptions', function () {
|
|
$exception = new RuntimeException('Something went wrong');
|
|
|
|
$this->monitor->recordFromException($exception);
|
|
|
|
expect(SecurityAttempt::count())->toBe(0);
|
|
});
|
|
|
|
it('never throws exceptions itself', function () {
|
|
$exception = new CannotUpdateLockedPropertyException('test');
|
|
|
|
// Create a request that might cause issues
|
|
$request = Request::create('/test', 'POST');
|
|
$request->setRouteResolver(fn () => null);
|
|
|
|
// This should not throw even if there are issues
|
|
$this->monitor->recordFromException($exception, $request);
|
|
|
|
// If we get here without exception, the test passes
|
|
expect(true)->toBeTrue();
|
|
});
|
|
|
|
it('tracks attempts from same IP', function () {
|
|
$exception = new CannotUpdateLockedPropertyException('test');
|
|
|
|
$request = Request::create('/livewire/update', 'POST', [
|
|
'components' => [
|
|
['snapshot' => '{}', 'updates' => []],
|
|
],
|
|
], server: ['REMOTE_ADDR' => '192.168.1.100']);
|
|
|
|
$request->setRouteResolver(fn () => null);
|
|
|
|
// Record multiple attempts
|
|
$this->monitor->recordFromException($exception, $request);
|
|
$this->monitor->recordFromException($exception, $request);
|
|
$this->monitor->recordFromException($exception, $request);
|
|
|
|
expect($this->monitor->getAttemptsFromIp('192.168.1.100'))->toBe(3);
|
|
});
|
|
|
|
it('identifies suspicious IPs', function () {
|
|
$exception = new CannotUpdateLockedPropertyException('test');
|
|
|
|
$request = Request::create('/livewire/update', 'POST', [
|
|
'components' => [
|
|
['snapshot' => '{}', 'updates' => []],
|
|
],
|
|
], server: ['REMOTE_ADDR' => '10.0.0.1']);
|
|
|
|
$request->setRouteResolver(fn () => null);
|
|
|
|
// Record 10 attempts (threshold)
|
|
for ($i = 0; $i < 10; $i++) {
|
|
$this->monitor->recordFromException($exception, $request);
|
|
}
|
|
|
|
expect($this->monitor->isIpSuspicious('10.0.0.1', threshold: 10))->toBeTrue()
|
|
->and($this->monitor->isIpSuspicious('10.0.0.2', threshold: 10))->toBeFalse();
|
|
});
|
|
|
|
it('truncates long values', function () {
|
|
$exception = new CannotUpdateLockedPropertyException('test');
|
|
|
|
$longUserAgent = str_repeat('a', 1000);
|
|
|
|
$request = Request::create('/livewire/update', 'POST', [
|
|
'components' => [
|
|
['snapshot' => '{}', 'updates' => []],
|
|
],
|
|
], server: ['HTTP_USER_AGENT' => $longUserAgent]);
|
|
|
|
$request->setRouteResolver(fn () => null);
|
|
|
|
$this->monitor->recordFromException($exception, $request);
|
|
|
|
$attempt = SecurityAttempt::first();
|
|
expect(strlen($attempt->user_agent))->toBeLessThanOrEqual(500);
|
|
});
|
|
|
|
it('records a security attempt when a locked-property exception is reported through the handler', function () {
|
|
$exception = new CannotUpdateLockedPropertyException('isLoggedIn');
|
|
|
|
app(ExceptionHandler::class)->report($exception);
|
|
|
|
expect(SecurityAttempt::count())->toBe(1)
|
|
->and(SecurityAttempt::first()->target_property)->toBe('isLoggedIn');
|
|
});
|
|
|
|
it('does not forward locked-property exceptions to the default log stack', function () {
|
|
Log::spy();
|
|
|
|
app(ExceptionHandler::class)->report(new CannotUpdateLockedPropertyException('isLoggedIn'));
|
|
|
|
expect(SecurityAttempt::count())->toBe(1);
|
|
|
|
Log::shouldNotHaveReceived('log');
|
|
Log::shouldNotHaveReceived('error');
|
|
Log::shouldNotHaveReceived('critical');
|
|
Log::shouldNotHaveReceived('warning');
|
|
});
|
|
|
|
it('still forwards non-security exceptions to the default log stack', function () {
|
|
Log::spy();
|
|
|
|
app(ExceptionHandler::class)->report(new RuntimeException('boom'));
|
|
|
|
expect(SecurityAttempt::count())->toBe(0);
|
|
|
|
Log::shouldHaveReceived('error');
|
|
});
|
|
|
|
it('handles X-Forwarded-For header', function () {
|
|
$exception = new CannotUpdateLockedPropertyException('test');
|
|
|
|
$request = Request::create('/livewire/update', 'POST', [
|
|
'components' => [
|
|
['snapshot' => '{}', 'updates' => []],
|
|
],
|
|
], server: [
|
|
'REMOTE_ADDR' => '127.0.0.1',
|
|
'HTTP_X_FORWARDED_FOR' => '203.0.113.50, 70.41.3.18',
|
|
]);
|
|
|
|
$request->setRouteResolver(fn () => null);
|
|
|
|
$this->monitor->recordFromException($exception, $request);
|
|
|
|
$attempt = SecurityAttempt::first();
|
|
expect($attempt->ip_address)->toBe('203.0.113.50');
|
|
});
|