security: Host-Whitelist & Legacy-API-Key-Übergabe härten (H5, M11)

- trustHosts erlaubt nur die konfigurierten Domains (plus localhost/IP
  für Health-Checks) und verwirft fremde Host-Header, sodass
  X-Forwarded-Host keine URL-Generierung mehr vergiften kann (H5).
- Legacy-API-Keys nur noch per Header (Bearer/X-ApiKey); die alte
  Query-/Body-Übergabe wird nicht mehr zur Auth akzeptiert, erhält aber
  weiterhin den 410-Migrationshinweis statt eines opaken 401 (M11).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Kevin Adametz 2026-07-14 13:48:28 +00:00
parent 000ae93e3b
commit 299837d2ba
9 changed files with 207 additions and 21 deletions

View file

@ -20,6 +20,8 @@ class LegacyCompanyController extends Controller
public function store(Request $request): JsonResponse
{
abort_unless($request->user()->tokenCan('press-releases:write'), 403);
try {
$validated = $request->validate([
'company' => ['required', 'array'],

View file

@ -19,6 +19,8 @@ class LegacyContactController extends Controller
public function store(Request $request, int $company): JsonResponse
{
abort_unless($request->user()->tokenCan('press-releases:write'), 403);
$company = Company::withoutGlobalScopes()
->whereKey($company)
->accessibleBy($request->user())

View file

@ -9,8 +9,8 @@ use Symfony\Component\HttpFoundation\Response;
/**
* Legacy-API-Kompatibilität (Übernahme bestehender Integrationen,
* 06.07.2026): Die Altportale authentifizierten per `X-ApiKey`-Header bzw.
* `api_key`-Parameter. Importierte Legacy-Keys (legacy:import-api-keys)
* 06.07.2026): Die Altportale authentifizierten per `X-ApiKey`-Header.
* Importierte Legacy-Keys (legacy:import-api-keys)
* sind gültige Sanctum-Tokens — hier wird die alte Übergabeform in einen
* Bearer-Header übersetzt, damit `auth:sanctum` sie versteht. Unbekannte
* Keys erhalten weiterhin 410 mit Migrationshinweis.
@ -27,6 +27,14 @@ class RejectLegacyApiKeys
$legacyKey = $this->legacyKey($request);
if ($legacyKey === null) {
// Kein Header-Key: ein in Query/Body übergebener Alt-Schlüssel wird
// NICHT für die Auth akzeptiert (Zugangsdaten dürfen nicht in URLs/
// Logs landen), erhält aber weiterhin den Migrationshinweis statt
// eines nichtssagenden 401 — sofern kein regulärer Bearer-Token da ist.
if (blank($request->bearerToken()) && $this->hasParameterKey($request)) {
return $this->migrationHintResponse();
}
return $next($request);
}
@ -41,27 +49,40 @@ class RejectLegacyApiKeys
return $next($request);
}
return response()->json([
'message' => 'This API key is not valid (anymore). Please generate a token in your customer account.',
'migration_url' => url('/customer/tokens'),
'docs_url' => url('/docs/api/v1'),
], 410);
return $this->migrationHintResponse();
}
/**
* Legacy-Schlüssel aus den historischen Übergabeformen: `X-ApiKey`
* (dokumentierter Header der Altportale), `X-Api-Key` (verbreitete
* Schreibvariante) oder `api_key` als Query-/Body-Parameter.
* Legacy-Schlüssel ausschließlich aus Headern lesen. Query- und
* Body-Parameter werden bewusst nicht akzeptiert, damit Zugangsdaten
* nicht in URLs, Proxy-Logs oder Referrer-Headern landen.
*/
private function legacyKey(Request $request): ?string
{
$key = trim((string) (
$request->header('X-ApiKey')
?: $request->header('X-Api-Key')
?: $request->query('api_key')
?: $request->input('api_key')
));
return $key === '' ? null : $key;
}
/**
* Prüft, ob ein Alt-Schlüssel per Query- oder Body-Parameter übergeben
* wurde. Wird nur genutzt, um den Migrationshinweis auszulösen — der Wert
* wird nie zur Authentifizierung verwendet.
*/
private function hasParameterKey(Request $request): bool
{
return filled($request->query('api_key')) || filled($request->input('api_key'));
}
private function migrationHintResponse(): Response
{
return response()->json([
'message' => 'This API key is not valid (anymore). Please generate a token in your customer account and send it as a Bearer token or X-ApiKey header.',
'migration_url' => url('/customer/tokens'),
'docs_url' => url('/docs/api/v1'),
], 410);
}
}