diff --git a/app/Http/Controllers/ProfileImageController.php b/app/Http/Controllers/ProfileImageController.php
index ff5523e6..76bf22b9 100644
--- a/app/Http/Controllers/ProfileImageController.php
+++ b/app/Http/Controllers/ProfileImageController.php
@@ -21,6 +21,12 @@ class ProfileImageController extends Controller
{
$user = Auth::user();
+ // Das Foto haengt am Konto (users.account_id). Ohne Konto gibt es nichts
+ // zu speichern — sonst liefe der Zugriff darunter in einen Fatal.
+ if (! $user->account) {
+ abort(404);
+ }
+
if (! Request::get('profile_image_consent')) {
\Session()->flash('alert-danger', __('account.profile_image_consent_required'));
@@ -66,7 +72,7 @@ class ProfileImageController extends Controller
{
$user = Auth::user();
- if ($user->account->profile_image) {
+ if ($user->account?->profile_image) {
\Storage::disk('public')->delete('images/profile/'.$user->account->profile_image);
$user->account->profile_image = null;
diff --git a/app/Services/Slim.php b/app/Services/Slim.php
index 33c3d8c4..902828ad 100644
--- a/app/Services/Slim.php
+++ b/app/Services/Slim.php
@@ -17,7 +17,17 @@ class Slim
/**
* Speichert ein Slim-Cropper-Upload (Base64-Payload aus getImages()) im
* public-Disk und liefert Dateiname + Metadaten — gemeinsame Logik für
- * Shop- und Profilfoto-Upload. Null bei nicht erlaubtem Dateityp.
+ * Shop- und Profilfoto-Upload. Null, wenn der Inhalt kein JPEG/PNG ist.
+ *
+ * Zwei Dinge sind hier sicherheitsrelevant und dürfen nicht vereinfacht
+ * werden:
+ *
+ * 1. `output.type` ist eine ANGABE DES CLIENTS aus dem geposteten JSON, kein
+ * geprüfter Inhaltstyp. Massgeblich ist deshalb, was
+ * getimagesizefromstring() in den Bytes findet.
+ * 2. Die Endung kommt IMMER aus dem geprüften Typ, nie aus dem übermittelten
+ * Dateinamen: {@see sanitizeFileName()} lässt jede Endung stehen, ein
+ * "foto.php" landete sonst als ausführbarer Dateiname im public-Disk.
*
* @param array $image Ein Eintrag aus {@see getImages()}
* @return array{filename: string, originalname: string, ext: string, mime: string, size: mixed}|null
@@ -26,19 +36,31 @@ class Slim
{
$allowed = ['image/jpeg' => 'jpg', 'image/png' => 'png'];
- if (! isset($allowed[$image['output']['type']])) {
+ $declaredType = $image['output']['type'] ?? null;
+ $data = $image['output']['data'] ?? null;
+
+ if (! is_string($declaredType) || ! isset($allowed[$declaredType]) || ! is_string($data) || $data === '') {
return null;
}
- $name = uniqid().'_'.Slim::sanitizeFileName($image['output']['name']);
+ $info = @getimagesizefromstring($data);
+ $mime = is_array($info) ? ($info['mime'] ?? null) : null;
- \Storage::disk('public')->put(rtrim($directory, '/').'/'.$name, $image['output']['data']);
+ if (! is_string($mime) || ! isset($allowed[$mime])) {
+ return null;
+ }
+
+ $ext = $allowed[$mime];
+ $base = Slim::sanitizeFileName(pathinfo((string) ($image['output']['name'] ?? ''), PATHINFO_FILENAME));
+ $name = uniqid().'_'.$base.'.'.$ext;
+
+ \Storage::disk('public')->put(rtrim($directory, '/').'/'.$name, $data);
return [
'filename' => $name,
- 'originalname' => $image['output']['name'],
- 'ext' => $allowed[$image['output']['type']],
- 'mime' => $image['output']['type'],
+ 'originalname' => $image['output']['name'] ?? '',
+ 'ext' => $ext,
+ 'mime' => $mime,
'size' => $image['input']['size'] ?? null,
];
}
@@ -118,7 +140,7 @@ class Slim
if (isset($data->output)) {
- $outputDate = null;
+ $outputData = null;
if (isset($data->output->image)) {
$outputData = Slim::getBase64Data($data->output->image);
} elseif (isset($data->output->field)) {
diff --git a/resources/views/user/components/user_profile_image.blade.php b/resources/views/user/components/user_profile_image.blade.php
index 98a27996..6ba2dcc7 100644
--- a/resources/views/user/components/user_profile_image.blade.php
+++ b/resources/views/user/components/user_profile_image.blade.php
@@ -14,9 +14,15 @@
'size' => 120,
])
-
- {{ __('account.profile_image_delete') }}
+ {{-- Loeschen per POST: als GET-Link liesse sich das Foto ueber ein
+ fremdes loeschen (CSRF). --}}
+