set('app.trusted_proxies', ['127.0.0.1', '::1']); $middleware = new CloudflareProxies; $trustedRequest = Request::create('/rooms', 'GET', server: [ 'REMOTE_ADDR' => '127.0.0.1', 'HTTP_CF_CONNECTING_IP' => '203.0.113.10', ]); $middleware->handle($trustedRequest, fn () => response('ok')); $this->assertSame('203.0.113.10', $trustedRequest->server->get('REMOTE_ADDR')); $untrustedRequest = Request::create('/rooms', 'GET', server: [ 'REMOTE_ADDR' => '198.51.100.25', 'HTTP_CF_CONNECTING_IP' => '203.0.113.11', ]); $middleware->handle($untrustedRequest, fn () => response('ok')); $this->assertSame('198.51.100.25', $untrustedRequest->server->get('REMOTE_ADDR')); } /** * 验证 Banner 广播会将危险 HTML 净化为纯文本后再下发。 */ public function test_banner_broadcast_sanitizes_user_controlled_html(): void { $fakeBroadcastFactory = new class { /** * 记录控制器最终广播出去的事件对象。 */ public ?BannerNotification $capturedEvent = null; /** * 截获 broadcast() helper 传入的事件实例。 */ public function event($event): PendingBroadcast { $this->capturedEvent = $event; return new class($event) extends PendingBroadcast { /** * 构造一个不会二次派发的占位 PendingBroadcast。 */ public function __construct(mixed $event) { parent::__construct(app('events'), $event); } /** * 测试替身无需在析构时再次派发事件。 */ public function __destruct() {} }; } /** * 满足广播工厂抽象依赖,当前测试不会走到该分支。 */ public function connection($name = null): never { throw new \RuntimeException('SecurityHardeningTest 不应调用广播连接。'); } }; $this->app->instance(BroadcastFactory::class, $fakeBroadcastFactory); $request = Request::create('/admin/banner/broadcast', 'POST', [ 'target' => 'room', 'target_id' => 1, 'options' => [ 'title' => '危险标题', 'name' => '管理员', 'body' => '危险正文', 'sub' => '副标题', 'titleColor' => 'url(javascript:1)', 'gradient' => ['#123456', 'rgba(1,2,3,0.5)', 'url(javascript:alert(1))'], 'buttons' => [ [ 'label' => '立即处理', 'color' => 'rgba(16,185,129,1)', 'action' => 'close', ], ], ], ]); $request->headers->set('Accept', 'application/json'); $response = app(BannerBroadcastController::class)->send($request); $this->assertSame(200, $response->getStatusCode()); $this->assertSame('success', $response->getData(true)['status']); $this->assertInstanceOf(BannerNotification::class, $fakeBroadcastFactory->capturedEvent); $this->assertSame('危险标题', $fakeBroadcastFactory->capturedEvent->options['title']); $this->assertSame('管理员', $fakeBroadcastFactory->capturedEvent->options['name']); $this->assertSame('危险正文', $fakeBroadcastFactory->capturedEvent->options['body']); $this->assertSame('副标题', $fakeBroadcastFactory->capturedEvent->options['sub']); $this->assertStringNotContainsString('"', $fakeBroadcastFactory->capturedEvent->options['titleColor']); $this->assertStringNotContainsString('javascript', $fakeBroadcastFactory->capturedEvent->options['gradient'][2]); $this->assertSame('立即处理', $fakeBroadcastFactory->capturedEvent->options['buttons'][0]['label']); } /** * 验证 Horizon 面板仅允许站长账号访问。 */ public function test_horizon_gate_only_allows_site_owner(): void { $provider = new class($this->app) extends HorizonServiceProvider { /** * 显式注册测试所需的 Horizon gate。 */ public function registerTestGate(): void { $this->gate(); } }; $provider->registerTestGate(); $siteOwner = new User([ 'username' => 'site-owner', 'user_level' => 1, ]); $siteOwner->id = 1; $revokedManager = new User([ 'username' => 'revoked-manager', 'user_level' => 100, ]); $this->assertTrue(Gate::forUser($siteOwner)->allows('viewHorizon')); $this->assertFalse(Gate::forUser($revokedManager)->allows('viewHorizon')); } /** * 验证前台登录入口在命中限流后会直接返回 429。 */ public function test_chat_login_route_returns_429_when_rate_limited(): void { $rateLimitKey = md5('chat-login'.'chat-login|testuser|127.0.0.1'); RateLimiter::clear($rateLimitKey); foreach (range(1, 5) as $attempt) { RateLimiter::hit($rateLimitKey, 60); } $response = $this->withoutMiddleware(ValidateCsrfToken::class) ->withServerVariables(['REMOTE_ADDR' => '127.0.0.1']) ->postJson('/login', [ 'username' => 'TestUser', 'password' => 'secret', 'captcha' => 'invalid', ]); $response->assertStatus(429) ->assertJsonPath('status', 'error'); RateLimiter::clear($rateLimitKey); } /** * 验证隐藏后台登录入口在命中限流后会直接拒绝请求。 */ public function test_hidden_admin_login_route_returns_429_when_rate_limited(): void { $rateLimitKey = md5('admin-hidden-login'.'admin-hidden-login|site-owner|127.0.0.1'); RateLimiter::clear($rateLimitKey); foreach (range(1, 5) as $attempt) { RateLimiter::hit($rateLimitKey, 60); } $response = $this->withoutMiddleware(ValidateCsrfToken::class) ->from('/lkddi') ->withServerVariables(['REMOTE_ADDR' => '127.0.0.1']) ->post('/lkddi', [ 'username' => 'site-owner', 'password' => 'secret', 'captcha' => 'invalid', ]); $response->assertStatus(429); $this->assertStringContainsString('/lkddi', $response->headers->get('Location', '')); RateLimiter::clear($rateLimitKey); } }