Artisan Développeur

Qualité, amélioration continue, agilité, professionnalisme.

[php] How to manage uploaded images in secure way

I dev lately on an open source project, GRR (https://github.com/JeromeDevome/GRR/), it’s an old resources management script.

Writen a looooong time ago, « old school » code, but functional and pretty complete.

A security researcher , « kmkz » (Bourbon Jean-marie) – twitter: @kmkz_security, has discovered a security flaw which permit to upload a shell using the upload logo functionality.
Just need to rename the image like EvilShell.php.jpg …
The old code, never check if the submitted file have more than one extension, or if it’s an actual image… Just check if the final extension is jpg, gif or png.

After lot of reading about mime type, EXIF code injection, magic bytes, and million of stackoverflow questions, my little function :

https://github.com/nicolas-san/upload

This is a small, not POO, simple function to check :

  • multiple extension (logo.php.png)
  • extension allowed (jpg, png or gif)
  • good mime type with php finfo_file method http://php.net/manual/en/intro.fileinfo.php
  • if image is valid and remove EXIF with one of the imagecreateFORMAT function from GD http://php.net/manual/en/function.imagecreatefrompng.php
  • if target directory is writable
  • if move_uploaded_file pass without error

The idea is to start test with the obvious attempts, remove the EXIF data to be sure and rename the file to avoid long and wired names.

I think I cover all attack vectors with an image, but if you want test it, break it, improve or comment, do it !

<?php

/**
 * @author Bouteillier Nicolas <contact@kaizendo.fr>
 * Date: 13/01/16
 *
 * @license MIT
 *
 * @Param string $varName Name of the form input var
 *
 * @Param string $directoryImagePath Path of the directory where to save the image ex: '../images/' - The final / is needed
 * 
 * @Return string|int $pictureName string name of the new image or error code
 *
 * Error codes are :
 * -1 if varName ! isset
 * -2 if more than one extension ex: image.php.jpg
 * -3 extension not allowed (other than jpg, gif and png)
 * -4 mime type not allowed (other than image/gif, image/jpeg,image/png )
 * -5 imagecreate* failed, image not valid
 * -6 $directoryImagePath is not writable
 * -7 move_uploaded_file error
 * -8 can't delete moved file (you have to check if the file is still there and delete it)
 * -9 can't save GD created image, or destroy the return of imagecreate*
 *
 */

function saveUploadedFile($varName, $directoryImagePath)
{
    if (isset($_FILES[$varName])) {
        $file = $_FILES[$varName];
        /* first test only to block more than one extension file */
        if (count(explode('.', $file['name'])) > 2) {
            return -2;
        } elseif (preg_match("`\.([^.]+)$`", $file['name'], $match)) {
            /* here file has just one extension */
            $ext = strtolower($match[1]);
            if ($ext != 'jpg' && $ext != 'png' && $ext != 'gif') {
                return -3;
            } else {
                /**
                 * extension is ok
                 * third test with fileinfo to get the mime type with magic bytes
                 */
                $finfo = finfo_open(FILEINFO_MIME_TYPE);
                $fileType = finfo_file($finfo, $file['tmp_name']);
                /* fourth test depends on the extension, imagecreate with gd, to check if the image is valid and remove all exif data to avoid code injection */
                switch ($fileType) {
                    case 'image/gif':
                        $logoRecreated = @imagecreatefromgif($file['tmp_name']);
                        /* fix for transparency */
                        imageAlphaBlending($logoRecreated, true);
                        imageSaveAlpha($logoRecreated, true);
                        $extSafe = 'gif';
                        break;
                    case 'image/jpeg':
                        $logoRecreated = @imagecreatefromjpeg($file['tmp_name']);
                        $extSafe = 'jpg';
                        break;
                    case 'image/png':
                        $logoRecreated = @imagecreatefrompng($file['tmp_name']);
                        /* fix for transparency */
                        imageAlphaBlending($logoRecreated, true);
                        imageSaveAlpha($logoRecreated, true);
                        $extSafe = 'png';
                        break;
                    default:
                        return -4;
                        break;
                }
                if (!$logoRecreated) {
                    /* imagecreate* failed, the image is not good */
                    return -5;
                } else {
                    /** 
                     * valid image, good mime type
                     * destination is writable ?
                    */
                    /* generate random failename */
                    $randName = md5(uniqid(rand(), true));
                    $pictureName = $randName.'.'.$extSafe;
                    $picturePath = $directoryImagePath.$pictureName;

                    if (is_writable($directoryImagePath)) {
                        /* usage of move_uploaded_file to check on more time if the file is good (is it too much ?) */
                        $moveUploadReturn = move_uploaded_file($file['tmp_name'], $picturePath);
                        if (!$moveUploadReturn) {
                            return -7;
                        } else {
                            /* move_uploaded_file return is ok, I delete the file, and use the GD created exif free file instead */
                            $unlinkReturn = unlink($picturePath);
                            if (!$unlinkReturn) {
                                return -8;
                            } else {
                                /* the file is deleted, saving the new image */
                                switch ($extSafe) {
                                    case 'gif':
                                        $retourSaveImage = imagegif($logoRecreated, $picturePath);
                                        break;
                                    case 'jpg':
                                        $retourSaveImage = imagejpeg($logoRecreated, $picturePath);
                                        break;
                                    case 'png':
                                        $retourSaveImage = imagepng($logoRecreated, $picturePath);
                                        break;
                                }
                                $retourDestroy = imagedestroy($logoRecreated);
                                if (!$retourSaveImage || !$retourDestroy) {
                                    return -9;
                                }
                                /*
                                 * All tests are passed, the function return the path of the new image
                                 * */
                                return $pictureName;
                            }
                        }
                    } else {
                        return -6;
                    }
                }
            }
        }
    } else {
        return -1;
    }
}

var_dump($_FILES);
echo "<br>";

echo saveUploadedFile('file', './');

 

KVM – resize volume disk virt-manager – virt-resize

If you have install a new window 7 virtual machine to do some tests, very very quickly, you can have the same problem is had.

8 GB volume disk is not enough…

First boot of the vm, windows complains about space left, if you want to do updates, it’s too late 🙂

I used to be more virtualbox for my little needs, most for web test under several browser under native os. But, I’m not the oracle biggest fan, so go to kvm.

I use virt-manager to setup vms, and it’s very simple.

Now I want to resize my little volume, and have more space for my win7 test vm.

  1. install libguestfs-tools
  2. create a new volume
  3. resize
  4. add new volume
  5. Profits !

1 – libguestfs-tools

In a terminal (konsole under kde) try virt-resize, if you don’t have it do :

apt-get install libguestfs-tools

2 – create new volume

First see what are your pools :

virsh pool-list

Result :

 Nom                  État      Démarrage automatique
-------------------------------------------
 2to                  actif      yes   
  
 isos                 actif      yes       

I want use the pool named « 2to ».

Create the new volume on the « 2to » pool with virsh:

virsh vol-create-as --format qcow2 2to win7homepremium.qcow2 15G

I choose to keep the qcow2 format, because the input volume is in qcow2.

3 – Resize

Take a look at the partitions in the current volume :

Search for the volume :

virsh dumpxml win7 | grep source

« win7 » is my guestname.

Output is :

<source file='/media/raid/vms/window7.qcow2'/>
<source network='default'/>

See the partitions :

virt-filesystems --long --parts --blkdevs -h -a /media/raid/vms/window7.qcow2

Name       Type       MBR  Size  Parent
/dev/sda1  partition  07   100M  /dev/sda
/dev/sda2  partition  07   7,9G  /dev/sda
/dev/sda   device     -    8,0G  -

We need to resize the volume, but virt-resize copy input to output, and create a new partition with the extra space, we need to use –expand option on /dev/sda2 :

virt-resize --expand /dev/sda2 /media/raid/vms/window7.qcow2 /media/raid/vms/win7homepremium.qcow2
[   0,0] Examining /media/raid/vms/window7.qcow2
**********

Summary of changes:

/dev/sda1: This partition will be left alone.

/dev/sda2: This partition will be resized from 7,9G to 14,9G.  The 
filesystem ntfs on /dev/sda2 will be expanded using the 'ntfsresize' 
method.

**********
[   4,0] Setting up initial partition table on /media/raid/vms/win7homepremium.qcow2
[   4,0] Copying /dev/sda1
[   4,0] Copying /dev/sda2
 100% ⟦▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒⟧ 00:00
[  28,0] Expanding /dev/sda2 using the 'ntfsresize' method

Resize operation completed with no errors.  Before deleting the old disk, 
carefully check that the resized disk boots and works correctly.

At this point you have a new 15GB volume, check the partitions :

virt-filesystems --long --parts --blkdevs -h -a /media/raid/vms/win7homepremium.qcow2
Name       Type       MBR  Size  Parent
/dev/sda1  partition  07   100M  /dev/sda
/dev/sda2  partition  07   15G   /dev/sda
/dev/sda   device     -    15G   -

All good !

4 – Add the new volume with virt-manager

I know, I know, I can do the same with a better « sysadmin way » with my terminal, but hey, I’m not a terminal freak ^^

  • Go to the vm details and create a new device with the right mouse clic:

kvm - virt-manager to create a new device

  • Select the second option : existing storage and « browse » (parcourir in french)

kvm - new disk to resize the old one

  • Search/select your new create qcow2 volume, and confirm:

KVM - virt manager - create a new disk

  • last step you have to remove/delete old « Disk1 » because windows have some signature device conflit if you try to boot with both disks:

Delete virtual disk kvm

After that you can boot your vm and check disks :

I’ve to reboot windows7 the first boot after the resize, the finish install new device.

6-newDisk

All good, new size ! I can do updates, and use my testing vm.

virt-resize complete doc

Error installing cordova with npm under ubuntu 15.10

If you try install cordova like this :

npm install -g cordova

And you have some access file error :

npm ERR! tar.unpack untar error /home/user/.npm/cordova/5.4.0/package.tgz
npm ERR! Error: EACCES, mkdir '/usr/local/lib/node_modules'
npm ERR!  { [Error: EACCES, mkdir '/usr/local/lib/node_modules']
npm ERR!   errno: 3,
npm ERR!   code: 'EACCES',
npm ERR!   path: '/usr/local/lib/node_modules',
npm ERR!   fstream_type: 'Directory',
npm ERR!   fstream_path: '/usr/local/lib/node_modules/cordova',
npm ERR!   fstream_class: 'DirWriter',
npm ERR!   fstream_stack: 
npm ERR!    [ '/usr/lib/nodejs/fstream/lib/writer.js:171:23',
npm ERR!      '/usr/lib/nodejs/mkdirp/index.js:46:53',
npm ERR!      'Object.oncomplete (fs.js:107:15)' ] }
npm ERR! 
npm ERR! Please try running this command again as root/Administrator.

npm ERR! System Linux 4.2.0-18-generic
npm ERR! command "/usr/bin/nodejs" "/usr/bin/npm" "install" "-g" "cordova"
npm ERR! cwd /home/user
npm ERR! node -v v0.10.25
npm ERR! npm -v 1.4.21
npm ERR! path /usr/local/lib/node_modules
npm ERR! fstream_path /usr/local/lib/node_modules/cordova
npm ERR! fstream_type Directory
npm ERR! fstream_class DirWriter
npm ERR! code EACCES
npm ERR! errno 3
npm ERR! stack Error: EACCES, mkdir '/usr/local/lib/node_modules'
npm ERR! fstream_stack /usr/lib/nodejs/fstream/lib/writer.js:171:23
npm ERR! fstream_stack /usr/lib/nodejs/mkdirp/index.js:46:53
npm ERR! fstream_stack Object.oncomplete (fs.js:107:15)
npm ERR! 
npm ERR! Additional logging details can be found in:
npm ERR!     /home/user/npm-debug.log
npm ERR! not ok code 0

You can just fix the group of files, and try again with sudo (because in the end, a symlink is created to /usr/bin/[…] )

sudo chown user:user -R /usr/local/lib/node_modules/
sudo chown -R user:user  /home/user/.npm

I don’t know if it’s the best way, and I don’t know if it’s ok for a production env, in my case, it’s just dev/play env 🙂