Skip to content

Commit bb6dd2a

Browse files
committed
fixes
1 parent 711cdf4 commit bb6dd2a

2 files changed

Lines changed: 143 additions & 11 deletions

File tree

src/Validation/IsImage.php

Lines changed: 51 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -31,8 +31,9 @@ public function __construct(
3131
$this->checkImageData = $checkImageData;
3232
$this->allowSvg = $allowSvg;
3333

34-
// If SVG is explicitly allowed, add it to allowed MIME types
35-
if( $this->allowSvg && !in_array( 'image/svg+xml', $this->allowedMimeTypes, true ) )
34+
// If SVG is explicitly allowed AND we have MIME type restrictions, add SVG to allowed list
35+
// Don't add if allowedMimeTypes is empty (meaning allow all types)
36+
if( $this->allowSvg && !empty( $this->allowedMimeTypes ) && !in_array( 'image/svg+xml', $this->allowedMimeTypes, true ) )
3637
{
3738
$this->allowedMimeTypes[] = 'image/svg+xml';
3839
}
@@ -74,7 +75,8 @@ protected function validate( mixed $value ) : bool
7475
private function validateDataUri( string $dataUri ) : bool
7576
{
7677
// Parse data URI: data:[<mediatype>][;base64],<data>
77-
$pattern = '/^data:([a-zA-Z0-9][a-zA-Z0-9\/+\-]*);base64,(.+)$/';
78+
// Use 's' modifier to allow . to match newlines in base64 data
79+
$pattern = '/^data:([a-zA-Z0-9][a-zA-Z0-9\/+\-]*);base64,(.+)$/s';
7880

7981
if( !preg_match( $pattern, $dataUri, $matches ) )
8082
{
@@ -119,22 +121,48 @@ private function validateBase64Image( string $base64Data ) : bool
119121
return false;
120122
}
121123

122-
// If we need to check the actual image data
123-
if( $this->checkImageData )
124+
// Always validate image data to check MIME type restrictions
125+
// Even when checkImageData is false, we need to detect the type for MIME validation
126+
// but we can skip the more expensive image content validation
127+
if( !empty( $this->allowedMimeTypes ) )
124128
{
129+
// Detect the image type from signatures
130+
$detectedType = $this->detectImageType( $decoded );
131+
132+
// Check for SVG if allowed
133+
if( $detectedType === null && $this->allowSvg )
134+
{
135+
$detectedType = $this->detectSvg( $decoded );
136+
}
137+
138+
// If we couldn't detect a type, it's not a valid image
139+
if( $detectedType === null )
140+
{
141+
return false;
142+
}
143+
144+
// Check if detected type is allowed
145+
if( !$this->isMimeTypeAllowed( $detectedType ) )
146+
{
147+
return false;
148+
}
149+
}
150+
elseif( $this->checkImageData )
151+
{
152+
// No MIME restrictions but need to validate image data
125153
return $this->validateImageData( $decoded );
126154
}
127155

128156
return true;
129157
}
130158

131159
/**
132-
* Validates that the decoded data is actually a valid image.
160+
* Detects image type from binary data signatures.
133161
*
134162
* @param string $imageData
135-
* @return bool
163+
* @return string|null Returns MIME type if detected, null otherwise
136164
*/
137-
private function validateImageData( string $imageData ) : bool
165+
private function detectImageType( string $imageData ) : ?string
138166
{
139167
// Check for common image file signatures (magic numbers)
140168
$signatures = [
@@ -152,7 +180,6 @@ private function validateImageData( string $imageData ) : bool
152180
$dataStart = substr( $imageData, 0, 20 );
153181

154182
// Check for image signatures
155-
$detectedType = null;
156183
foreach( $signatures as $signature => $mimeType )
157184
{
158185
if( strpos( $dataStart, $signature ) === 0 )
@@ -163,11 +190,24 @@ private function validateImageData( string $imageData ) : bool
163190
continue;
164191
}
165192

166-
$detectedType = $mimeType;
167-
break;
193+
return $mimeType;
168194
}
169195
}
170196

197+
return null;
198+
}
199+
200+
/**
201+
* Validates that the decoded data is actually a valid image.
202+
*
203+
* @param string $imageData
204+
* @return bool
205+
*/
206+
private function validateImageData( string $imageData ) : bool
207+
{
208+
// Use the extracted method to detect image type
209+
$detectedType = $this->detectImageType( $imageData );
210+
171211
// Check for SVG separately with more strict validation
172212
if( $detectedType === null && $this->allowSvg )
173213
{

tests/Validation/IsImageTest.php

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -324,6 +324,98 @@ public function testBomRemovalExactSequence()
324324
$this->assertTrue( $validatorWithSvg->isValid( $svgEfBase64 ) );
325325
}
326326

327+
/**
328+
* Test data URI with line-wrapped base64 (common in email/MIME).
329+
*/
330+
public function testDataUriWithLineWrappedBase64()
331+
{
332+
// PNG image with base64 wrapped at 76 characters (common MIME format)
333+
$pngBase64Wrapped = "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChw\nGA60e6kgAAAABJRU5ErkJggg==";
334+
335+
// Test with newline in base64 data
336+
$dataUriWithNewline = 'data:image/png;base64,' . $pngBase64Wrapped;
337+
338+
$validator = new IsImage();
339+
// Should pass - newlines are valid in base64 data URIs
340+
$this->assertTrue( $validator->isValid( $dataUriWithNewline ) );
341+
342+
// Test with multiple newlines and spaces (also valid)
343+
$pngBase64MultiLine = "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJ\n" .
344+
"AAAADUlEQVR42mNkYPhfDwAChw\n" .
345+
"GA60e6kgAAAABJRU5ErkJggg==";
346+
347+
$dataUriMultiLine = 'data:image/png;base64,' . $pngBase64MultiLine;
348+
$this->assertTrue( $validator->isValid( $dataUriMultiLine ) );
349+
350+
// Test with carriage return + newline (Windows style)
351+
$pngBase64CRLF = str_replace("\n", "\r\n", $pngBase64Wrapped);
352+
$dataUriCRLF = 'data:image/png;base64,' . $pngBase64CRLF;
353+
$this->assertTrue( $validator->isValid( $dataUriCRLF ) );
354+
}
355+
356+
/**
357+
* Test MIME type restrictions work for raw base64 input.
358+
*/
359+
public function testMimeTypeRestrictionsForRawBase64()
360+
{
361+
// PNG image as raw base64
362+
$pngBase64 = 'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChwGA60e6kgAAAABJRU5ErkJggg==';
363+
364+
// JPEG image as raw base64
365+
$jpegBase64 = '/9j/4AAQSkZJRgABAQEAYABgAAD/2wBDAAgGBgcGBQgHBwcJCQgKDBQNDAsLDBkSEw8UHRofHh0aHBwgJC4nICIsIxwcKDcpLDAxNDQ0Hyc5PTgyPC4zNDL/2wBDAQkJCQwLDBgNDRgyIRwhMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjL/wAARCAABAAEDASIAAhEBAxEB/8QAFQABAQAAAAAAAAAAAAAAAAAAAAr/xAAUEAEAAAAAAAAAAAAAAAAAAAAA/8QAFQEBAQAAAAAAAAAAAAAAAAAAAAX/xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwCdABmX/9k=';
366+
367+
// Validator that only allows JPEG, even with checkImageData=false
368+
$jpegOnlyValidator = new IsImage( [ 'image/jpeg' ], null, false );
369+
370+
// JPEG should pass
371+
$this->assertTrue( $jpegOnlyValidator->isValid( $jpegBase64 ) );
372+
373+
// PNG should fail even with checkImageData=false
374+
$this->assertFalse( $jpegOnlyValidator->isValid( $pngBase64 ) );
375+
376+
// Validator that only allows PNG
377+
$pngOnlyValidator = new IsImage( [ 'image/png' ], null, false );
378+
379+
// PNG should pass
380+
$this->assertTrue( $pngOnlyValidator->isValid( $pngBase64 ) );
381+
382+
// JPEG should fail
383+
$this->assertFalse( $pngOnlyValidator->isValid( $jpegBase64 ) );
384+
}
385+
386+
/**
387+
* Test empty allowedMimeTypes with SVG doesn't restrict to SVG only.
388+
*/
389+
public function testEmptyAllowedMimeTypesWithSvgAllowsAll()
390+
{
391+
// Empty array means allow all types
392+
$validatorAllowAll = new IsImage( [], null, true, true );
393+
394+
// All image types should pass
395+
$jpegBase64 = '/9j/4AAQSkZJRgABAQEAYABgAAD/2wBDAAgGBgcGBQgHBwcJCQgKDBQNDAsLDBkSEw8UHRofHh0aHBwgJC4nICIsIxwcKDcpLDAxNDQ0Hyc5PTgyPC4zNDL/2wBDAQkJCQwLDBgNDRgyIRwhMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjL/wAARCAABAAEDASIAAhEBAxEB/8QAFQABAQAAAAAAAAAAAAAAAAAAAAr/xAAUEAEAAAAAAAAAAAAAAAAAAAAA/8QAFQEBAQAAAAAAAAAAAAAAAAAAAAX/xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwCdABmX/9k=';
396+
$this->assertTrue( $validatorAllowAll->isValid( $jpegBase64 ) );
397+
398+
$pngBase64 = 'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChwGA60e6kgAAAABJRU5ErkJggg==';
399+
$this->assertTrue( $validatorAllowAll->isValid( $pngBase64 ) );
400+
401+
$gifBase64 = 'R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7';
402+
$this->assertTrue( $validatorAllowAll->isValid( $gifBase64 ) );
403+
404+
// SVG should also pass
405+
$svgBase64 = base64_encode( '<svg xmlns="http://www.w3.org/2000/svg"></svg>' );
406+
$this->assertTrue( $validatorAllowAll->isValid( $svgBase64 ) );
407+
408+
// Now test with allowSvg=false but empty allowedMimeTypes
409+
$validatorNoSvg = new IsImage( [], null, true, false );
410+
411+
// Non-SVG images should still pass
412+
$this->assertTrue( $validatorNoSvg->isValid( $jpegBase64 ) );
413+
$this->assertTrue( $validatorNoSvg->isValid( $pngBase64 ) );
414+
415+
// SVG should fail
416+
$this->assertFalse( $validatorNoSvg->isValid( $svgBase64 ) );
417+
}
418+
327419
/**
328420
* Test malformed data URI.
329421
*/

0 commit comments

Comments
 (0)