Security (R6.2): Haertung nach Audit
- Security-Header-Middleware (nosniff, X-Frame-Options, Referrer-/ Permissions-Policy) global, proxy-unabhaengig; CSP bewusst separat. - Firmenlogo-Upload im Create-Formular durch den ImageService-Pfad ersetzt (Mime-Whitelist ohne SVG, Re-Encode, UUID-Name) - vorher rohe store()-Ablage ohne Server-Validierung (SVG/XSS moeglich). - Rate-Limits ergaenzt: Passwort-Reset (3/E-Mail + 15/IP pro Std.), Registrierung (10/IP pro Std.), throttle auf magic-login- und pm-vorschau-Consume-Routen (Defense-in-Depth). - EnsureUserIsCustomer als persistente Livewire-Middleware (symmetrisch zum Admin-Gate, greift auch bei Folge-Requests). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
acb2a19aff
commit
89550dac99
9 changed files with 212 additions and 9 deletions
|
|
@ -3,6 +3,7 @@
|
|||
use App\Enums\CompanyType;
|
||||
use App\Enums\Portal;
|
||||
use App\Models\Company;
|
||||
use App\Services\Image\ImageService;
|
||||
use Illuminate\Support\Str;
|
||||
use Livewire\Attributes\Layout;
|
||||
use Livewire\Attributes\Title;
|
||||
|
|
@ -48,7 +49,7 @@ new #[Layout('components.layouts.app'), Title('Neue Firma')] class extends Compo
|
|||
#[Validate('required|max:2')]
|
||||
public string $country = 'DE';
|
||||
|
||||
#[Validate('nullable|image|max:1024')]
|
||||
#[Validate('nullable|image|max:4096')]
|
||||
public $logo;
|
||||
|
||||
#[Validate('nullable|max:255')]
|
||||
|
|
@ -61,17 +62,13 @@ new #[Layout('components.layouts.app'), Title('Neue Firma')] class extends Compo
|
|||
|
||||
public bool $is_active = true;
|
||||
|
||||
public function save(): void
|
||||
public function save(ImageService $imageService): void
|
||||
{
|
||||
$this->validate();
|
||||
|
||||
$slug = (new Company)->generateUniqueSlug($this->company_name, ['portal' => $this->portal]);
|
||||
|
||||
$logoPath = $this->logo
|
||||
? $this->logo->store('company-logos', 'public')
|
||||
: null;
|
||||
|
||||
Company::query()->create([
|
||||
$company = Company::query()->create([
|
||||
'portal' => $this->portal,
|
||||
'type' => $this->type,
|
||||
'name' => $this->company_name,
|
||||
|
|
@ -81,10 +78,24 @@ new #[Layout('components.layouts.app'), Title('Neue Firma')] class extends Compo
|
|||
'phone' => $this->phone ?: null,
|
||||
'email' => $this->email ?: null,
|
||||
'website' => $this->website ?: null,
|
||||
'logo_path' => $logoPath,
|
||||
'is_active' => $this->is_active,
|
||||
]);
|
||||
|
||||
// Logo wie im Edit-Pfad über den ImageService (Mime-Whitelist ohne
|
||||
// SVG, Re-Encode via GD, UUID-Name) — nie roh auf die public-Disk.
|
||||
if ($this->logo) {
|
||||
$stored = $imageService->storeCompanyLogo(
|
||||
$this->logo,
|
||||
$this->portal === Portal::Both->value ? Portal::Presseecho->value : $this->portal,
|
||||
$company->id,
|
||||
);
|
||||
|
||||
$company->update([
|
||||
'logo_path' => $stored['path'],
|
||||
'logo_variants' => $stored['variants'],
|
||||
]);
|
||||
}
|
||||
|
||||
session()->flash('success', 'Firma erfolgreich erstellt.');
|
||||
$this->redirect(route('admin.companies.index'), navigate: true);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,9 @@
|
|||
<?php
|
||||
|
||||
use Illuminate\Support\Facades\Password;
|
||||
use Illuminate\Support\Facades\RateLimiter;
|
||||
use Illuminate\Support\Str;
|
||||
use Illuminate\Validation\ValidationException;
|
||||
use Livewire\Attributes\Layout;
|
||||
use Livewire\Volt\Component;
|
||||
|
||||
|
|
@ -16,10 +19,41 @@ new #[Layout('components.layouts.auth.pressekonto', ['heading' => 'Passwort verg
|
|||
'email' => ['required', 'string', 'email'],
|
||||
]);
|
||||
|
||||
// Drosseln wie beim Anmeldelink: pro E-Mail (Mail-Fluten eines
|
||||
// Postfachs) und pro IP (Durchprobieren vieler Adressen). Der
|
||||
// Password-Broker drosselt zusätzlich nur pro Nutzer (60s).
|
||||
$this->ensureIsNotRateLimited();
|
||||
RateLimiter::hit($this->throttleKey(), 3600);
|
||||
RateLimiter::hit($this->ipThrottleKey(), 3600);
|
||||
|
||||
Password::sendResetLink($this->only('email'));
|
||||
|
||||
session()->flash('status', __('A reset link will be sent if the account exists.'));
|
||||
}
|
||||
|
||||
protected function ensureIsNotRateLimited(): void
|
||||
{
|
||||
$withinEmail = ! RateLimiter::tooManyAttempts($this->throttleKey(), 3);
|
||||
$withinIp = ! RateLimiter::tooManyAttempts($this->ipThrottleKey(), 15);
|
||||
|
||||
if ($withinEmail && $withinIp) {
|
||||
return;
|
||||
}
|
||||
|
||||
throw ValidationException::withMessages([
|
||||
'email' => __('Zu viele Anfragen. Bitte versuchen Sie es später erneut.'),
|
||||
]);
|
||||
}
|
||||
|
||||
protected function throttleKey(): string
|
||||
{
|
||||
return 'password-reset|'.Str::transliterate(Str::lower($this->email));
|
||||
}
|
||||
|
||||
protected function ipThrottleKey(): string
|
||||
{
|
||||
return 'password-reset-ip|'.request()->ip();
|
||||
}
|
||||
}; ?>
|
||||
|
||||
<div>
|
||||
|
|
|
|||
|
|
@ -4,7 +4,9 @@ use App\Models\User;
|
|||
use Illuminate\Auth\Events\Registered;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Illuminate\Support\Facades\Hash;
|
||||
use Illuminate\Support\Facades\RateLimiter;
|
||||
use Illuminate\Validation\Rules;
|
||||
use Illuminate\Validation\ValidationException;
|
||||
use Livewire\Attributes\Layout;
|
||||
use Livewire\Volt\Component;
|
||||
|
||||
|
|
@ -33,6 +35,16 @@ new #[Layout('components.layouts.auth.pressekonto', ['heading' => 'Konto erstell
|
|||
'terms_accepted.accepted' => 'Bitte bestätigen Sie unsere AGB und die Datenschutzerklärung.',
|
||||
]);
|
||||
|
||||
// Bot-/Massenanmeldungen drosseln (R6.2): pro IP, großzügig genug
|
||||
// für legitime Mehrfachversuche.
|
||||
if (RateLimiter::tooManyAttempts('register-ip|'.request()->ip(), 10)) {
|
||||
throw ValidationException::withMessages([
|
||||
'email' => __('Zu viele Registrierungen. Bitte versuchen Sie es später erneut.'),
|
||||
]);
|
||||
}
|
||||
|
||||
RateLimiter::hit('register-ip|'.request()->ip(), 3600);
|
||||
|
||||
// Die bestätigte AGB-/Datenschutz-Checkbox wird als DSGVO-Einwilligung
|
||||
// mit Zeitstempel festgehalten (users.gdpr_consent_at).
|
||||
unset($validated['terms_accepted']);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue