I've read various posts about how letting users to upload files can create vulnerabilities to your website such as a user injecting php code in an image.
So i've created a small test project where you can upload (outside of web root) and see uploaded images keeping it as simple as i could having in mind security but i'm not an expert and it would be really helpful if you could answer some of my questions and tell me if something could be done better.
-let's say that a malicious image bypasses my checks during uploading. When i fetch an image using
$directory = $_SERVER['DOCUMENT_ROOT'] . "/../secure_images/";
$images = glob($directory . "*.{[jJ][pP][gG],[pP][nN][gG],[jJ][pP][eE][gG]}", GLOB_BRACE);
$fileinfo = finfo_open(FILEINFO_MIME_TYPE);
for ($i = 0; $i < count($images); $i++)
{
$extention = finfo_file($fileinfo, $images[$i]);
header('Content-Type: ' . $extention);
data["images"][$i] = base64_encode(file_get_contents($images[$i]));
data["extention"][$i] = $extention;
}
echo json_encode($data);
get the response in js using ajax and then append it to the dom to be shown to the user using <img src='data:"+ data.extention[i] +";base64," + data.images[i] + "'>
is it possible to be harmful in any way?
-Fetching multiple images using base64_encode(file_get_contents($images[$i])) seems a bit slow and also the string that is being put inside img src is huge...can this be a problem (for example images don't appear in xiaomis MIUI browser)? Is there a better alternative?
-Is storing images outside of root trying to prevent access of malicious users too much of a hassle? Is it better maybe to just store them inside root and make use of .htaccess to prevent someone from doing harm? Would an .htaccess like the following be enough for that purpose?
#Deny access to .htaccess files
<Files .htaccess>
order allow,deny
deny from all
</Files>
#Enable the DirectoryIndex Protection, preventing directory index listings and defaulting
Options -Indexes
DirectoryIndex index.html index.php /index.php
#Securing directories: Remove the ability to execute scripts
AddHandler cgi-script .php .pl .py .jsp .asp .htm .shtml .sh .cgi
Options -ExecCGI
Bellow is my test project (working)
Structure
/
public_html (root) 755
.htaccess 444
index.php 644
images.php 644
javascript 755
show_images.js 644
upload.js 644
php_scripts 755
fetch_images.php 600
upload_images.php 600
logo.png 644
secure_images
.htaccess 444
201811051007191220027687.jpg 644
20181105100719574368017.jpeg 644
secure_php_scripts 500
fetch_images.php 600
upload_images.php 600
public_html/.htaccess
#Deny access to .htaccess files
<Files .htaccess>
order allow,deny
deny from all
</Files>
#Enable the DirectoryIndex Protection, preventing directory index listings and defaulting
Options -Indexes
DirectoryIndex index.html index.php /index.php
#Trackback Spam protection
RewriteCond %{REQUEST_METHOD} =POST
RewriteCond %{HTTP_USER_AGENT} ^.*(opera|mozilla|firefox|msie|safari).*$ [NC]
RewriteCond %{THE_REQUEST} ^[A-Z]{3,9}\ /.+/trackback/?\ HTTP/ [NC]
RewriteRule .? - [F,NS,L]
public_html/index.php
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Image upload security test</title>
<meta name="description" content="Image upload security test">
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.2.1/jquery.min.js"></script>
</head>
<body>
<form id="upload_form" method="post" enctype="multipart/form-data">
Select image to upload:
<input type="file" name="filesToUpload[]" id="filesToUpload" multiple>
<input type="submit" value="Upload Image" name="submit">
</form>
<a href="images.php">See images</a>
<script src="js/upload.js"></script>
</body>
</html>
public_html/images.php
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Image upload security test</title>
<meta name="description" content="Image upload security test">
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.2.1/jquery.min.js"></script>
<style>
section{display:block;text-align:center;}
content{display:inline-block;margin:10px;height:400px;width:400px;}
content img{max-height:100%;max-width:100%;min-height:100%;min-width:100%;}
</style>
</head>
<body>
<section>
</section>
<script src="js/show_images.js"></script>
</body>
</html>
public_html/javascript/show_images.js
window.onload = function() {
$.ajax({
url:"../php_scripts/fetch_images.php",
method:"POST",
dataType:"JSON",
success:function(data)
{
if(data.outcome) {
if(data.images)
{
if(data.images.length > 0)
{
let text = [];
for( let i=0; i< data.images.length; i++)
{
text[i] = "<content><img src='data:"+ data.extention[i] +";base64," + data.images[i] + "'></content>";
}
$("section").append(text);
}
}
else {console.log("no images found"); }
}
else { console.log("An error occured please try again later"); }
}
});
};
public_html/javascript/upload.js
$("#upload_form").submit(function(e)
{
e.preventDefault();
$.ajax({
url:"../php_scripts/upload_images.php",
method:"POST",
data:new FormData(this),
processData: false,
contentType: false,
dataType:"JSON",
success:function(data)
{
if(data.outcome) { console.log("Images succesfully uploaded"); }
else { console.log(data.msg); }
}
});
});
public_html/php_scripts/fetch_images.php
<?php
include_once($_SERVER['DOCUMENT_ROOT'] . "/../secure_php_scripts/fetch_images.php");
public_html/php_scripts/upload_images.php
<?php
include_once($_SERVER['DOCUMENT_ROOT'] . "/../secure_php_scripts/upload_images.php");
secure_php_scripts/fetch_images.php
<?php
try
{
$data["outcome"] = true;
$directory = $_SERVER['DOCUMENT_ROOT'] . "/../secure_images/";
$images = glob($directory . "*.{[jJ][pP][gG],[pP][nN][gG],[jJ][pP][eE][gG]}", GLOB_BRACE);
$fileinfo = finfo_open(FILEINFO_MIME_TYPE);
for ($i = 0; $i < count($images); $i++)
{
$extention = finfo_file($fileinfo, $images[$i]);
header('Content-Type: ' . $extention);
$data["extention"][$i] = $extention;
$data["images"][$i] = base64_encode(file_get_contents($images[$i]));
}
echo json_encode($data);
} catch(Exception $e)
{
$data["outcome"] = false;
$data["images"][0] = [];
echo json_encode($data);
}
secure_php_scripts/upload_images.php
<?php
$uploaded_images[0]["path"] = null;
try
{
$isValid = validateArray($_FILES['filesToUpload']);
if($isValid[0])
{
// Since the files are ok rename them and then upload
// Get extentions (checked in validateArray that they are valid)
$i = 0;
foreach ($_FILES['filesToUpload']['name'] as $item)
{
$ext[$i] = pathinfo($item, PATHINFO_EXTENSION);
$i++;
}
$i = 0;
foreach ($_FILES['filesToUpload']['tmp_name'] as $item)
{
$new_name = date('YmdHis',time()).mt_rand() . "." . $ext[$i];
$path_to_be_uploaded_to = $_SERVER['DOCUMENT_ROOT'] . "/../secure_images/" . $new_name;
$i++;
// Copy the uploaded image to a new image and also add a watermark
if(!add_watermark($item))
{
throw new Exception('There was an error uploading your file.');
break;
}
if (!move_uploaded_file($item, $path_to_be_uploaded_to))
{
throw new Exception('There was an error uploading your file.');
break;
}
else
{
$uploaded_images[$i]["path"] = $path_to_be_uploaded_to;
}
}
$data['outcome'] = true;
$data['msg'] = "Images uploaded successfully!";
echo json_encode($data);
}
else
{
$data['outcome'] = false;
$data['msg'] = $isValid[1];
echo json_encode($data);
}
} catch (Exception $e)
{
//If there is an exception delete all uploaded images
if($uploaded_images[0]["path"] != null)
{
foreach($uploaded_images as $item)
{
if( file_exists($item["path"]) ) { unlink($item["path"]); }
}
}
// Also delete all uploaded files from tmp folder (Files user uploads first go there)
foreach($_FILES['filesToUpload']['tmp_name'] as $item)
{
if( file_exists($item) ) { unlink($item); }
}
$data['outcome'] = false;
$data['msg'] = "There was an error please try again later";
echo json_encode($data);
}
// Create a new image blank using imagecreatetruecolor()
// Copy our image to the new image using imagecopyresampled()
// And also add a logo in the process
function add_watermark($path_to_img)
{
try
{
if(!file_exists ($path_to_img))
{
return false;
}
else if(exif_imagetype($path_to_img) == IMAGETYPE_JPEG)
{
$im = imagecreatefromjpeg($path_to_img);
$k=0;
}
else if(exif_imagetype($path_to_img) == IMAGETYPE_PNG)
{
$im = imagecreatefrompng($path_to_img);
$k=1;
}
else //not an image
{
return false;
}
$stamp = imagecreatefrompng('logo.png');
// Set the margins for the stamp and get the height/width of the stamp image
$marge_right = 10;
$marge_bottom = 10;
$sx = imagesx($stamp);
$sy = imagesy($stamp);
list($width, $height) = getimagesize($path_to_img);
$dest_imagex = 900;//width of new image
$dest_imagey = 900;//height of new image
$dest_image = imagecreatetruecolor($dest_imagex, $dest_imagey);//create new image
imagecopyresampled($dest_image, $im, 0, 0, 0, 0, $dest_imagex, $dest_imagey, $width,$height);//#im to $dest_image
//Now $dest_image is an image of 800x800
// Copy the stamp image onto our photo using the margin offsets and the photo width to calculate positioning of the stamp.
imagecopy($dest_image, $stamp, $dest_imagex - $sx - $marge_right, $dest_imagey - $sy - $marge_bottom, 0, 0, $sx, $sy);
$filename = $path_to_img;
if($k == 0)
{
imagejpeg($dest_image, $filename);
}
else
{
imagepng($dest_image, $filename);
}
return true;
} catch (Exception $e)
{
return false;
}
}
// Checks
// if the element provided is a 2D array with the expected elements (multiple pictures per upload)
// if there is an error
// if file extentions are the allowed ones
// is each file size is bellow 1GB
// if the file number is less than 16
// if file exists and if it was uploaded via HTTP POST
function validateArray($array)
{
try{
$allowedExts = array('png', 'jpeg', 'jpg');
$allowedExts2 = array('image/png', 'image/jpg', 'image/jpeg');
if( $array && is_array($array) )
{
if( !is_array($array['name'])) { return [false, "Wrong array format"]; }
else { $pic_number = count($array['name']); }
// if($pic_number > 15) { return [false, "Maximum image number allowed is 15"]; }
foreach ($array['name'] as $item)
{
if(is_array($item)) { return [false, "Wrong array format"]; }
$ext = pathinfo($item, PATHINFO_EXTENSION);
if( !in_array($ext, $allowedExts) ) { return [false, "Only PNG JPEG JPG images are allowed"]; }
}
if( !is_array($array['type']) || count($array['type']) != $pic_number ) { return [false, "Wrong array format"]; }
foreach ($array['type'] as $item)
{
if(is_array($item)) { return [false, "Wrong array format"]; }
}
if( !is_array($array['tmp_name']) || count($array['tmp_name']) != $pic_number ) { return [false, "Wrong array format"]; }
$fileinfo = finfo_open(FILEINFO_MIME_TYPE);
foreach ($array['tmp_name'] as $item)
{
if(is_array($item)) { return [false, "Wrong format"]; }
if(!file_exists($item) || !is_uploaded_file($item)) { return [false, "File doesn't exists, try again"]; }
if(!is_uploaded_file($item)) { return [false, "File has to be uploaded using our form"]; }
if(!exif_imagetype($item)) { return [false, "Only images allowed"]; }
if(filesize($item) < 12) { return [false, "All images has to be more than 11 bytes"]; }
if (!in_array(finfo_file($fileinfo, $item), $allowedExts2)) { return [false, "Only PNG JPEG JPG images are allowed"]; }
}
if( !is_array($array['error']) || count($array['error']) != $pic_number ) { return [false, "Wrong array format"]; }
foreach ($array['error'] as $item)
{
if(is_array($item)) { return [false, "Wrong array format"]; }
if($item !== 0) { return [false, "File error"]; }
}
if( !is_array($array['size']) || count($array['size']) != $pic_number ) { return [false, "Wrong array format"]; }
foreach ($array['size'] as $item)
{
if(is_array($item)) { return [false, "Wrong array format"]; }
if($item > 1000000) { return [false, "Maximum image size allowed is 1GB"]; }
}
}
else
{
return [false, "Element provided is not a valid array"];
}
return [true, "Chill dude images are ok"];
} catch (Exception $e)
{
// echo $e->getMessage();
return [false, "There was an error please try again later"];
}
}
secure_images/.htaccess
#Deny access to .htaccess files
<Files .htaccess>
order allow,deny
deny from all
</Files>
#Enable the DirectoryIndex Protection, preventing directory index listings and defaulting
Options -Indexes
DirectoryIndex index.html index.php /index.php
#Securing directories: Remove the ability to execute scripts
AddHandler cgi-script .php .pl .py .jsp .asp .htm .shtml .sh .cgi
Options -ExecCGI