VYPR
Critical severity9.8GHSA Advisory· Published Jun 11, 2026

CodeIgniter4 has a validation bypass when uploading file extensions via `ext_in` rule

CVE-2026-48062

Description

CodeIgniter4 ext_in validation rule bypass allows arbitrary code execution via uploaded file with mismatched extension.

AI Insight

LLM-synthesized narrative grounded in this CVE's description and references.

CodeIgniter4 ext_in validation rule bypass allows arbitrary code execution via uploaded file with mismatched extension.

Vulnerability

The ext_in validation rule in CodeIgniter4 versions prior to 4.7.3 only checks the MIME-derived guessed extension instead of the client-provided filename extension [1]. This allows a file named shell.php containing GIF-like content to pass validation rules such as ext_in[avatar,gif] because guessExtension() returns gif. The vulnerability is present in the ext_in rule implementation in system/Validation/StrictRules [4].

Exploitation

An attacker needs network access to an upload endpoint that uses ext_in for validation, saves files using the original client filename (e.g., $file->move($path)), stores uploads in a web-accessible directory, and allows execution of PHP (or other executable) files [2]. The attacker uploads a file with a dangerous extension (e.g., shell.php) but with content that triggers a safe MIME type (e.g., GIF). The ext_in rule passes because it only checks the guessed extension, and the file is saved with the original .php extension [3].

Impact

Successful exploitation leads to arbitrary code execution with the privileges of the web server. The attacker can execute arbitrary commands, access or modify sensitive data, and potentially gain further access to the server [3]. The default application does not expose such an upload endpoint, but custom implementations are vulnerable.

Mitigation

Upgrade to CodeIgniter4 version 4.7.3 or later, released on 2026-05-22, which fixes the ext_in rule to validate both the client extension and ensure it matches the guessed extension [3]. If upgrading is not immediately possible, workarounds include saving uploads outside the public web root, using $file->store() or $file->move($path, $file->getRandomName()) to avoid original filenames, disabling script execution in public upload directories, manually checking $file->getClientExtension(), and rejecting files where it does not match $file->guessExtension() [1][2].

AI Insight generated on Jun 11, 2026. Synthesized from this CVE's description and the cited reference URLs; citations are validated against the source bundle.

Affected products

1

Patches

1
29299349e7d2

Merge commit from fork

https://github.com/codeigniter4/CodeIgniter4Michal SniatalaMay 20, 2026via ghsa-ref
5 files changed · +119 3
  • system/Validation/StrictRules/FileRules.php+8 1 modified
    @@ -211,7 +211,14 @@ public function ext_in(?string $blank, string $params): bool
                     return true;
                 }
     
    -            if (! in_array($file->guessExtension(), $params, true)) {
    +            // Check the real filename extension, not only the guessed extension.
    +            $clientExtension = strtolower($file->getClientExtension());
    +
    +            if ($clientExtension === '' || ! in_array($clientExtension, $params, true)) {
    +                return false;
    +            }
    +
    +            if ($file->guessExtension() !== $clientExtension) {
                     return false;
                 }
             }
    
  • tests/system/Validation/StrictRules/FileRulesTest.php+82 0 modified
    @@ -316,6 +316,20 @@ public function testExtensionOk(): void
             $this->assertTrue($this->validation->run([]));
         }
     
    +    public function testExtensionOkWithMatchingClientExtensionAndMimeType(): void
    +    {
    +        $payload = $this->createGifPayload();
    +
    +        try {
    +            $this->setUploadedAvatar($payload, 'my-avatar.gif');
    +
    +            $this->validation->setRules(['avatar' => 'ext_in[avatar,gif]']);
    +            $this->assertTrue($this->validation->run([]));
    +        } finally {
    +            unlink($payload);
    +        }
    +    }
    +
         public function testExtensionNotOk(): void
         {
             $this->validation->setRules(['avatar' => 'ext_in[avatar,xls,doc,ppt]']);
    @@ -327,4 +341,72 @@ public function testExtensionImpossible(): void
             $this->validation->setRules(['avatar' => 'ext_in[unknown,xls,doc,ppt]']);
             $this->assertFalse($this->validation->run([]));
         }
    +
    +    public function testExtensionFailsForMismatchedClientExtension(): void
    +    {
    +        $payload = $this->createGifPayload();
    +
    +        try {
    +            $this->setUploadedAvatar($payload, 'shell.php');
    +
    +            $this->validation->setRules(['avatar' => 'ext_in[avatar,gif]']);
    +            $this->assertFalse($this->validation->run([]));
    +        } finally {
    +            unlink($payload);
    +        }
    +    }
    +
    +    public function testExtensionFailsForAllowedButMimeIncompatibleClientExtension(): void
    +    {
    +        $payload = $this->createGifPayload();
    +
    +        try {
    +            $this->setUploadedAvatar($payload, 'my-avatar.jpg');
    +
    +            $this->validation->setRules(['avatar' => 'ext_in[avatar,jpg,gif]']);
    +            $this->assertFalse($this->validation->run([]));
    +        } finally {
    +            unlink($payload);
    +        }
    +    }
    +
    +    public function testExtensionFailsForExtensionlessClientFilename(): void
    +    {
    +        $payload = $this->createGifPayload();
    +
    +        try {
    +            $this->setUploadedAvatar($payload, 'my-avatar');
    +
    +            $this->validation->setRules(['avatar' => 'ext_in[avatar,gif]']);
    +            $this->assertFalse($this->validation->run([]));
    +        } finally {
    +            unlink($payload);
    +        }
    +    }
    +
    +    private function createGifPayload(): string
    +    {
    +        $payload = tempnam(sys_get_temp_dir(), 'ci4-upload-poc-');
    +        $this->assertIsString($payload);
    +
    +        $gif = base64_decode('R0lGODlhAQABAIAAAAAAAP///ywAAAAAAQABAAACAUwAOw==', true);
    +        $this->assertIsString($gif);
    +
    +        file_put_contents($payload, $gif);
    +
    +        return $payload;
    +    }
    +
    +    private function setUploadedAvatar(string $payload, string $name): void
    +    {
    +        service('superglobals')->setFilesArray([
    +            'avatar' => [
    +                'tmp_name' => $payload,
    +                'name'     => $name,
    +                'size'     => filesize($payload),
    +                'type'     => 'image/gif',
    +                'error'    => UPLOAD_ERR_OK,
    +            ],
    +        ]);
    +    }
     }
    
  • user_guide_src/source/changelogs/v4.7.3.rst+11 0 modified
    @@ -10,6 +10,17 @@ Release Date: Unreleased
         :local:
         :depth: 3
     
    +********
    +SECURITY
    +********
    +
    +- **Validation:** The ``ext_in`` file upload validation rule now validates the
    +  client filename extension and verifies that it matches the detected MIME type.
    +  Previously, ``ext_in`` only checked the MIME-derived guessed extension, so a
    +  file with a mismatched client extension could pass validation.
    +  See the `Security advisory GHSA-2gr4-ppc7-7mhx <https://github.com/codeigniter4/CodeIgniter4/security/advisories/GHSA-2gr4-ppc7-7mhx>`_
    +  for more information.
    +
     ********
     BREAKING
     ********
    
  • user_guide_src/source/installation/upgrade_473.rst+14 0 modified
    @@ -30,6 +30,20 @@ upgrading. The easiest way is to re-run the install command:
     Breaking Changes
     ****************
     
    +File Validation
    +===============
    +
    +The ``ext_in`` file upload validation rule now checks the client filename
    +extension and verifies that the detected MIME type is associated with that
    +extension. Previously, ``ext_in`` only checked the MIME-derived guessed
    +extension.
    +
    +This means files with no client filename extension, or files whose client
    +filename extension does not match the detected MIME type, now fail ``ext_in``
    +validation. If your application intentionally accepts such files, remove
    +``ext_in`` from those validation rules and use a custom validation rule that
    +matches your application's requirements.
    +
     *********************
     Breaking Enhancements
     *********************
    
  • user_guide_src/source/libraries/validation.rst+4 2 modified
    @@ -1108,8 +1108,10 @@ min_dims                Yes         Fails if the minimum width and height of an
                                         v4.6.0.)
     mime_in                 Yes         Fails if the file's mime type is not one     ``mime_in[field_name,image/png,image/jpeg]``
                                         listed in the parameters.
    -ext_in                  Yes         Fails if the file's extension is not one     ``ext_in[field_name,png,jpg,gif]``
    -                                    listed in the parameters.
    +ext_in                  Yes         Fails if the client filename's extension is  ``ext_in[field_name,png,jpg,gif]``
    +                                    not one listed in the parameters, or if the
    +                                    detected MIME type does not match the MIME
    +                                    types associated with the extension.
     is_image                Yes         Fails if the file cannot be determined to be ``is_image[field_name]``
                                         an image based on the mime type.
     ======================= ========== ============================================= ===================================================
    

Vulnerability mechanics

Root cause

"The `ext_in` upload validation rule checked the MIME-derived guessed extension instead of the client-provided filename extension, allowing a file with a dangerous extension to pass validation."

Attack vector

An attacker uploads a file with a dangerous extension (e.g., `shell.php`) whose content matches an allowed MIME type (e.g., `image/gif`). The `ext_in` rule previously checked only the MIME-derived guessed extension (`gif`), so the file passed validation. If the application saves the file using the original client filename (`$file->move($path)`) in a web-accessible directory where PHP execution is enabled, the attacker can access the uploaded file and achieve arbitrary code execution [ref_id=1]. The advisory explicitly states this scenario leads to "arbitrary code execution" [ref_id=1].

Affected code

The `ext_in` method in `system/Validation/StrictRules/FileRules.php` (and the equivalent in `system/Validation/FileRules.php`) is at fault. Previously, the method only checked `$file->guessExtension()` (the MIME-derived extension) and ignored the client-provided filename extension entirely [patch_id=5618905].

What the fix does

The patch adds two checks in `ext_in()`: first, it retrieves the client filename extension via `$file->getClientExtension()` and rejects the file if that extension is empty or not in the allowed list; second, it verifies that `$file->guessExtension()` matches the client extension [patch_id=5618905]. This closes the gap because a file named `shell.php` with GIF content will have a client extension of `php` (not in the allowed list) and a guessed extension of `gif` (which does not match `php`), so validation fails. The upgrade notes and changelog confirm this behavioral change [patch_id=5618905].

Preconditions

  • configApplication must accept user-controlled file uploads
  • configApplication must use ext_in validation rule to validate the uploaded filename extension
  • configApplication must save uploaded files using the original client filename via $file->move($path)
  • configUpload directory must be web-accessible and allow execution of PHP (or other executable) files

Generated on Jun 11, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.

References

6

News mentions

0

No linked articles in our index yet.