Profilfoto: Upload nach Inhalt pruefen, Loeschen nur per POST
- Slim::storeCroppedImage() vertraute dem vom Client gemeldeten Typ und uebernahm die Endung aus dem Dateinamen; "shell.php" mit Typ image/png landete als .php im public-Disk. Jetzt entscheidet getimagesizefromstring() ueber die Bytes, die Endung kommt aus dem geprueften Typ. - Foto loeschen war ein GET-Link und liess sich per fremdem <img src> ausloesen (CSRF); jetzt POST-Formular mit Token. - Ohne Konto (users.account_id) bricht Upload/Loeschen mit 404 ab statt mit einem Fatal. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
22dcc0f972
commit
e009b11259
5 changed files with 90 additions and 14 deletions
|
|
@ -29,6 +29,16 @@ function piMakeUser(): User
|
|||
]);
|
||||
}
|
||||
|
||||
/** Echte Bytes eines 1x1-JPEG bzw. -PNG — storeCroppedImage() prueft den Inhalt, nicht die Client-Angabe. */
|
||||
function piImageBytes(string $type): string
|
||||
{
|
||||
$base64 = $type === 'png'
|
||||
? 'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8z8BQDwAEhQGAhKmMIQAAAABJRU5ErkJggg=='
|
||||
: '/9j/4AAQSkZJRgABAQEAYABgAAD/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wAARCAABAAEDASIAAhEBAxEB/8QAHwAAAQUBAQEBAQEAAAAAAAAAAAECAwQFBgcICQoL/8QAtRAAAgEDAwIEAwUFBAQAAAF9AQIDAAQRBRIhMUEGE1FhByJxFDKBkaEII0KxwRVS0fAkM2JyggkKFhcYGRolJicoKSo0NTY3ODk6Q0RFRkdISUpTVFVWV1hZWmNkZWZnaGlqc3R1dnd4eXqDhIWGh4iJipKTlJWWl5iZmqKjpKWmp6ipqrKztLW2t7i5usLDxMXGx8jJytLT1NXW19jZ2uHi4+Tl5ufo6ery8/T19vf4+fr/xAAfAQADAQEBAQEBAQEBAAAAAAAAAQIDBAUGBwgJCgv/xAC1EQACAQIEBAMEBwUEBAABAncAAQIDEQQFITEGEkFRB2FxEyIygQgUQpGhscEJIzNS8BVictEKFiQ04SXxFxgZGiYnKCkqNTY3ODk6Q0RFRkdISUpTVFVWV1hZWmNkZWZnaGlqc3R1dnd4eXqCg4SFhoeIiYqSk5SVlpeYmZqio6Slpqeoqaqys7S1tre4ubrCw8TFxsfIycrS09TV1tfY2dri4+Tl5ufo6ery8/T19vf4+fr/2gAMAwEAAhEDEQA/APP6KKKAP//Z';
|
||||
|
||||
return base64_decode($base64);
|
||||
}
|
||||
|
||||
it('verweigert den Foto-Upload ohne Einwilligung', function (): void {
|
||||
URL::resolveMissingNamedRoutesUsing(fn (string $name, $parameters = []) => '/'.$name);
|
||||
Storage::fake('public');
|
||||
|
|
@ -48,7 +58,7 @@ it('speichert ein Slim-Upload nur fuer jpg/png und liefert die Metadaten', funct
|
|||
|
||||
$image = [
|
||||
'input' => ['size' => 1234],
|
||||
'output' => ['data' => 'BINARYDATA', 'name' => 'Mein Foto!.jpg', 'type' => 'image/jpeg'],
|
||||
'output' => ['data' => piImageBytes('jpg'), 'name' => 'Mein Foto!.jpg', 'type' => 'image/jpeg'],
|
||||
];
|
||||
|
||||
$stored = Slim::storeCroppedImage($image, 'images/profile');
|
||||
|
|
@ -63,6 +73,38 @@ it('speichert ein Slim-Upload nur fuer jpg/png und liefert die Metadaten', funct
|
|||
expect(Slim::storeCroppedImage($gif, 'images/profile'))->toBeNull();
|
||||
});
|
||||
|
||||
it('legt die Datei-Endung nach dem geprueften Inhalt an, nicht nach dem Dateinamen', function (): void {
|
||||
// Angriffsbild: der Client deklariert image/png, nennt die Datei aber .php.
|
||||
// Frueher landete genau dieser Name im oeffentlichen Disk.
|
||||
Storage::fake('public');
|
||||
|
||||
$image = [
|
||||
'input' => ['size' => 10],
|
||||
'output' => ['data' => piImageBytes('png'), 'name' => 'shell.php', 'type' => 'image/png'],
|
||||
];
|
||||
|
||||
$stored = Slim::storeCroppedImage($image, 'images/profile');
|
||||
|
||||
expect($stored)->not->toBeNull()
|
||||
->and($stored['filename'])->toEndWith('.png')
|
||||
->and($stored['filename'])->not->toContain('.php');
|
||||
Storage::disk('public')->assertExists('images/profile/'.$stored['filename']);
|
||||
});
|
||||
|
||||
it('lehnt einen Upload ab, dessen Inhalt kein Bild ist', function (): void {
|
||||
// Die Typ-Angabe kommt aus dem geposteten JSON und ist damit frei waehlbar —
|
||||
// massgeblich sind die Bytes.
|
||||
Storage::fake('public');
|
||||
|
||||
$image = [
|
||||
'input' => ['size' => 10],
|
||||
'output' => ['data' => '<?php echo 1;', 'name' => 'foto.png', 'type' => 'image/png'],
|
||||
];
|
||||
|
||||
expect(Slim::storeCroppedImage($image, 'images/profile'))->toBeNull();
|
||||
expect(Storage::disk('public')->allFiles('images/profile'))->toBe([]);
|
||||
});
|
||||
|
||||
it('loescht das Profilfoto samt Datei', function (): void {
|
||||
URL::resolveMissingNamedRoutesUsing(fn (string $name, $parameters = []) => '/'.$name);
|
||||
Storage::fake('public');
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue