3
0
mirror of https://github.com/snipe/snipe-it.git synced 2026-08-18 19:22:48 +00:00
Files
snipe-it/app/Http/Middleware/EnforceApiUserAgent.php
snipe 7e617bbfa3 Switch to stripos(...) === 0
Updated the pattern check to use `stripos(...) === 0` (prefix match) instead of !== false (substring match), so scripted clients identify themselves at position 0, and prefix matching prevents a pattern from accidentally matching an unrelated UA that mentions it later. This makes surwe a UA like MyWrapper/1.0 (uses curl/8.5.0 internally) passes, and a plain curl/8.5.0 is blocked.
2026-06-24 12:18:39 +01:00

85 lines
2.9 KiB
PHP

<?php
namespace App\Http\Middleware;
use App\Models\Setting;
use Closure;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Symfony\Component\HttpFoundation\Response;
class EnforceApiUserAgent
{
public function handle(Request $request, Closure $next): Response
{
$setting = Setting::getSettings();
$rawUserAgent = $request->header('User-Agent');
$userAgent = trim((string) $rawUserAgent);
// Handle the blank-UA case up front. Blank blocking is independent of the
// pattern master so an admin can leave it off for integrations that legitimately
// send a blank User-Agent (e.g. Entra SCIM provisioning).
if ($userAgent === '') {
if ($setting?->block_blank_api_user_agents) {
return $this->reject($rawUserAgent);
}
return $next($request);
}
// From here on we know the UA is non-blank.
// Pattern-based blocking is gated behind block_api_user_agents so the textarea
// can be pre-populated with sensible defaults without auto-enabling blocking.
if (! $setting?->block_api_user_agents) {
return $next($request);
}
// Prefix match (=== 0) rather than substring: scripted clients identify
// themselves at the start of the User-Agent (curl/x, PostmanRuntime/x,
// python-requests/x, etc.), and matching only the prefix prevents a pattern
// from accidentally hitting an unrelated UA that mentions it later in the
// string.
foreach ($this->blockedPatterns($setting->blocked_api_user_agents) as $pattern) {
if (stripos($userAgent, $pattern) === 0) {
return $this->reject($rawUserAgent);
}
}
return $next($request);
}
/**
* Split the textarea contents into a clean list of patterns. Lines are
* trimmed and blanks are dropped so the textarea ergonomics (trailing
* newlines, accidental whitespace) don't accidentally match every UA.
*
* @return array<int, string>
*/
private function blockedPatterns(?string $raw): array
{
if ($raw === null || trim($raw) === '') {
return [];
}
return array_values(array_filter(array_map(
'trim',
preg_split('/\r\n|\r|\n/', $raw) ?: [],
), fn (string $line) => $line !== ''));
}
/**
* Echo the original, untrimmed User-Agent header back to the caller so they
* can see exactly what the server saw. Null means the header was absent.
*/
private function reject(?string $userAgent): JsonResponse
{
return new JsonResponse([
'status' => 'error',
'messages' => trans('admin/settings/general.blocked_api_user_agent_rejected'),
'payload' => [
'user_agent' => $userAgent,
],
], Response::HTTP_FORBIDDEN);
}
}