check()) { return $next($request); } $canaries = config('clusev.honeypot.canaries', []); if ($canaries === []) { return $next($request); } // A small, bounded set of candidate values — request BODY only (form-encoded via post() AND a // JSON body via json()), never the query string (which a reflected link could carry to ban a // victim), no recursion into nested arrays. $candidates = []; foreach (self::CANDIDATE_KEYS as $key) { foreach ([$request->post($key), $request->json($key)] as $value) { if (is_string($value) && $value !== '') { $candidates[] = $value; } } } if (($bearer = $request->bearerToken()) !== null && $bearer !== '') { $candidates[] = $bearer; } if (($auth = $request->header('Authorization')) !== null && $auth !== '') { $candidates[] = $auth; } foreach ($canaries as $name => $canaryValue) { if (! is_string($canaryValue) || $canaryValue === '') { continue; } foreach ($candidates as $candidate) { if (hash_equals($canaryValue, $candidate)) { return $this->trip($request, (string) $name); } } } return $next($request); } private function trip(Request $request, string $canaryName): Response { $ip = (string) $request->ip(); $this->guard->banNow($ip, 'honeytoken'); AuditEvent::create([ 'user_id' => null, 'actor' => 'system', 'action' => 'security.honeytoken_used', 'target' => $canaryName, 'ip' => $ip, 'meta' => ['path' => $request->path()], ]); return response()->view('errors.blocked', [], 403); } }