Skip to content

Commit 45a7972

Browse files
authored
Solidate mime type guessing. (#4056)
* Improve security of mime type guessing. * Update mime guessing changes in user guide and changelogs. * Add ext-fileinfo to suggest section of composer.json.
1 parent 453af97 commit 45a7972

File tree

11 files changed

+55
-19
lines changed

11 files changed

+55
-19
lines changed

CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,12 @@
1010
- `CodeIgniter\Config\Config` is now deprecated in favor of `CodeIgniter\Config\Factories::config()`
1111
- HTTP Layer Refactor: Numerous deprecations have been made towards a transition to a PSR-compliant HTTP layer. [See the User Guide](user_guide_src/source/installation/upgrade_405.rst)
1212

13+
**Mime Type Detection**
14+
15+
- `Config\Mimes::guessExtensionFromType` now only reverse searches the `$mimes` array if no extension is proposed (i.e., usually not for uploaded files).
16+
- The fallback values of `UploadedFile->getExtension()` and `UploadedFile->guessExtension()` have been changed. `UploadedFile->getExtension()` now returns `$this->getClientExtension()` instead of `''`; `UploadedFile->guessExtension()` now returns `''` instead of `$this->getClientExtension()`.
17+
These changes increase security when handling uploaded files as the client can no longer force a wrong mime type on the application. However, these might affect how file extensions are detected in your application.
18+
1319
## [v4.0.4](https://github.com/codeigniter4/CodeIgniter4/tree/v4.0.4) (2020-07-15)
1420

1521
[Full Changelog](https://github.com/codeigniter4/CodeIgniter4/compare/v4.0.3...v4.0.4)

admin/framework/composer.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,9 @@
2222
"predis/predis": "^1.1",
2323
"squizlabs/php_codesniffer": "^3.3"
2424
},
25+
"suggest": {
26+
"ext-fileinfo": "Improves mime type detection for files"
27+
},
2528
"autoload": {
2629
"psr-4": {
2730
"CodeIgniter\\": "system/"

admin/module/composer.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,9 @@
1212
"mikey179/vfsstream": "^1.6",
1313
"phpunit/phpunit": "^8.5"
1414
},
15+
"suggest": {
16+
"ext-fileinfo": "Improves mime type detection for files"
17+
},
1518
"autoload-dev": {
1619
"psr-4": {
1720
"Tests\\Support\\": "tests/_support"

admin/starter/composer.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,9 @@
1313
"mikey179/vfsstream": "^1.6",
1414
"phpunit/phpunit": "^8.5"
1515
},
16+
"suggest": {
17+
"ext-fileinfo": "Improves mime type detection for files"
18+
},
1619
"autoload": {
1720
"psr-4": {
1821
"App\\": "app",

app/Config/Mimes.php

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,9 @@
1212
* the most common one should be first in the array to aid the guess*
1313
* methods. The same applies when more than one mime-type exists for a
1414
* single extension.
15+
*
16+
* When working with mime types, please make sure you have the ´fileinfo´
17+
* extension enabled to reliably detect the media types.
1518
*/
1619

1720
class Mimes
@@ -33,7 +36,6 @@ class Mimes
3336
'text/csv',
3437
'text/x-comma-separated-values',
3538
'text/comma-separated-values',
36-
'application/octet-stream',
3739
'application/vnd.ms-excel',
3840
'application/x-csv',
3941
'text/x-csv',
@@ -69,7 +71,6 @@ class Mimes
6971
'application/pdf',
7072
'application/force-download',
7173
'application/x-download',
72-
'binary/octet-stream',
7374
],
7475
'ai' => [
7576
'application/pdf',
@@ -462,12 +463,10 @@ class Mimes
462463
'srt' => [
463464
'text/srt',
464465
'text/plain',
465-
'application/octet-stream',
466466
],
467467
'vtt' => [
468468
'text/vtt',
469469
'text/plain',
470-
'application/octet-stream',
471470
],
472471
'ico' => [
473472
'image/x-icon',
@@ -509,11 +508,20 @@ public static function guessExtensionFromType(string $type, string $proposedExte
509508

510509
$proposedExtension = trim(strtolower($proposedExtension));
511510

512-
if ($proposedExtension !== '' && array_key_exists($proposedExtension, static::$mimes) && in_array($type, is_string(static::$mimes[$proposedExtension]) ? [static::$mimes[$proposedExtension]] : static::$mimes[$proposedExtension], true))
511+
if ($proposedExtension !== '')
513512
{
514-
return $proposedExtension;
513+
if(array_key_exists($proposedExtension, static::$mimes) && in_array($type, is_string(static::$mimes[$proposedExtension]) ? [static::$mimes[$proposedExtension]] : static::$mimes[$proposedExtension], true))
514+
{
515+
// The detected mime type matches with the proposed extension.
516+
return $proposedExtension;
517+
}
518+
519+
// An extension was proposed, but the media type does not match the mime type list.
520+
return null;
515521
}
516522

523+
// Reverse check the mime type list if no extension was proposed.
524+
// This search is order sensitive!
517525
foreach (static::$mimes as $ext => $types)
518526
{
519527
if ((is_string($types) && $types === $type) || (is_array($types) && in_array($type, $types, true)))

composer.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,9 @@
2424
"rector/rector": "^0.8",
2525
"squizlabs/php_codesniffer": "^3.3"
2626
},
27+
"suggest": {
28+
"ext-fileinfo": "Improves mime type detection for files"
29+
},
2730
"config": {
2831
"optimize-autoloader": true,
2932
"preferred-install": "dist",

system/HTTP/Files/UploadedFile.php

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -308,24 +308,29 @@ public function getTempName(): string
308308
* Overrides SPLFileInfo's to work with uploaded files, since
309309
* the temp file that's been uploaded doesn't have an extension.
310310
*
311-
* Is simply an alias for guessExtension for a safer method
312-
* than simply relying on the provided extension.
313-
* Additionally it will return clientExtension in case if there are
314-
* other extensions with the same mime type.
311+
* This method tries to guess the extension from the files mime
312+
* type but will return the clientExtension if it fails to do so.
313+
*
314+
* This method will always return a more or less helpfull extension
315+
* but might be insecure if the mime type is not machted. Consider
316+
* using guessExtension for a more safe version.
315317
*/
316318
public function getExtension(): string
317319
{
318-
return $this->guessExtension();
320+
return $this->guessExtension() ?: $this->getClientExtension();
319321
}
320322

321323
/**
322-
* Attempts to determine the best file extension.
324+
* Attempts to determine the best file extension from the file's
325+
* mime type. In contrast to getExtension, this method will return
326+
* an empty string if it fails to determine an extension instead of
327+
* falling back to the unsecure clientExtension.
323328
*
324329
* @return string
325330
*/
326331
public function guessExtension(): string
327332
{
328-
return Mimes::guessExtensionFromType($this->getMimeType(), $this->getClientExtension()) ?? $this->getClientExtension();
333+
return Mimes::guessExtensionFromType($this->getMimeType(), $this->getClientExtension()) ?? '';
329334
}
330335

331336
//--------------------------------------------------------------------

system/Validation/FileRules.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -263,7 +263,7 @@ public function ext_in(?string $blank, string $params): bool
263263
return true;
264264
}
265265

266-
if (! in_array($file->getExtension(), $params, true))
266+
if (! in_array($file->guessExtension(), $params, true))
267267
{
268268
return false;
269269
}

tests/system/HTTP/Files/FileCollectionTest.php

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -185,11 +185,11 @@ public function testExtensionGuessing()
185185
$this->assertEquals('txt', $file->getExtension());
186186

187187
// proposed extension matches finfo_open mime type (text/plain)
188-
// but not client mime type
189188
$file = $collection->getFile('userfile2');
190189
$this->assertInstanceOf(UploadedFile::class, $file);
191190
$this->assertEquals('txt', $file->getExtension());
192-
$this->assertNotEquals('txt', \Config\Mimes::guessExtensionFromType($file->getClientMimeType(), $file->getClientExtension()) ?? $file->getClientExtension());
191+
// but not client mime type
192+
$this->assertEquals(null, \Config\Mimes::guessExtensionFromType($file->getClientMimeType(), $file->getClientExtension()));
193193

194194
// proposed extension does not match finfo_open mime type (text/plain)
195195
// but can be resolved by reverse searching
@@ -206,8 +206,11 @@ public function testExtensionGuessing()
206206
// this is a zip file (userFile4) but hat been renamed to 'rar'
207207
$file = $collection->getFile('userfile5');
208208
$this->assertInstanceOf(UploadedFile::class, $file);
209-
$this->assertNotEquals('rar', $file->getExtension());
210-
$this->assertEquals('rar', \Config\Mimes::guessExtensionFromType($file->getClientMimeType(), $file->getClientExtension()) ?? $file->getClientExtension());
209+
// getExtension falls back to clientExtension (insecure)
210+
$this->assertEquals('rar', $file->getExtension());
211+
$this->assertEquals('rar', \Config\Mimes::guessExtensionFromType($file->getClientMimeType(), $file->getClientExtension()));
212+
// guessExtension is secure and does not returns empty
213+
$this->assertEquals('', $file->guessExtension());
211214
}
212215

213216
//--------------------------------------------------------------------

user_guide_src/source/changelogs/v4.0.5.rst

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@ Enhancements:
1212
- New URL helper function ``url_is()`` which allows you to check the current URL to see if matches the given string.
1313
- Services now have their config parameters strictly typehinted. This will ensure no one will pass a different config instance. If you need to pass a new config with additional properties, you need to extend that particular config.
1414
- Support for setting SameSite attribute on Session and CSRF cookies has been added. For security and compatibility with latest browser versions, the default setting is ``Lax``.
15+
- Guessing file extensions from mime type in ``Config\Mimes::guessExtensionFromType`` now only reverse searches the ``$mimes`` array if no extension is proposed (i.e., usually not for uploaded files).
16+
- The getter functions for file extensions of uploaded files now have different fallback values (``$this->getClientExtension()`` for ``UploadedFile->getExtension()`` and ``''`` for ``UploadedFile->guessExtension()``). This is a security fix and makes the process less dependent on the client.
1517

1618
Changes:
1719

0 commit comments

Comments
 (0)