File upload security and web shells
A file upload feature (profile photo, product image, CV, supporting document) is one of the riskiest points of a website. If uploads are not checked thoroughly enough, a script file that runs when called from a browser can be placed on the server. Files of this kind are called web shells: a backdoor that gives an attacker the ability to read and modify files and run commands on the server. A web shell can be planted not only through an upload form but also through a compromised FTP account or a vulnerability in an outdated plugin.
The defence has two pillars: accepting uploads under strict rules, and making sure that no script can run in the folder where uploaded files are kept. The second prevents harm even if a mistake is made in the first.
In brief
- The file type is verified from the content; only types on the allowlist are accepted.
- The server generates the file name; files are stored outside the web root or in a folder where execution is disabled.
- Unexpected PHP files, changed dates and strange redirects are signs of a web shell.
- Before deleting a malicious file, copy it and close the entry point.
On this page
The rules of secure uploading
-
Verify the type from the content and use an allowlist
Do not trust the file type reported by the browser (
$_FILES[...]['type']) or the extension in the file name; both are under the user's control. Read the real type of the file on the server, from its content (finfo), and accept only the types you allow. The "these extensions are banned" approach (a blocklist) is not reliable; list what is allowed and reject the rest.
: Enlarge -
Decide the file name yourself
Do not use the file name supplied by the user on disk. Special characters, path separators and double extensions in the name (such as
document.php.jpg) are a source of problems. Save the file under a random name generated on the server, with the extension that corresponds to the type you verified; keep the original name in the database for display only.
: Enlarge -
Store files where execution is disabled
The safest place is a folder outside the web root: the file cannot be reached directly by URL and is served only through a script that checks authorisation. If files need to be served directly by URL (for example product images), disable script execution in the upload folder through the server configuration.
: Enlarge -
Limit the size, the number and who can upload
Set a limit on file size and on the number of files a user can upload; otherwise the disk can be filled up. If possible, make uploading available only to logged-in users, and protect the form with a CSRF token.
: Enlarge -
Recognise the signs of a web shell
Despite the precautions, a malicious file may have found its way onto the server. Unexpected PHP files, changed file dates and strange redirects are the first signs; the detailed list is in the "Signs of a web shell" section below.
: Enlarge
A secure upload example (PHP)
The example below handles the upload of a single image or PDF: it checks the upload error and the size, reads the type from the content, looks it up in the allowlist, generates a random name and moves the file to a folder outside the web root.
<?php
// Allowed types: real MIME type => extension to save with
$izinli = [
'image/jpeg' => 'jpg',
'image/png' => 'png',
'image/webp' => 'webp',
'application/pdf' => 'pdf',
];
$azamiBoyut = 5 * 1024 * 1024; // 5 MB
$hedefKlasor = dirname(__DIR__) . '/depo'; // OUTSIDE the web root
$dosya = $_FILES['belge'] ?? null;
if ($dosya === null || !is_uploaded_file($dosya['tmp_name']) || $dosya['error'] !== UPLOAD_ERR_OK) {
http_response_code(400);
echo 'The file could not be uploaded. Please try again.';
return;
}
if ($dosya['size'] <= 0 || $dosya['size'] > $azamiBoyut) {
http_response_code(400);
echo 'The file size can be at most 5 MB.';
return;
}
// Read the type from the content of the file, not from what the browser reports
$finfo = new finfo(FILEINFO_MIME_TYPE);
$tur = $finfo->file($dosya['tmp_name']);
if (!isset($izinli[$tur])) {
http_response_code(400);
echo 'Only JPG, PNG, WebP and PDF files are accepted.';
return;
}
// The server generates the name; the name supplied by the user is not used on disk
$yeniAd = bin2hex(random_bytes(16)) . '.' . $izinli[$tur];
if (!move_uploaded_file($dosya['tmp_name'], $hedefKlasor . '/' . $yeniAd)) {
error_log('Uploaded file could not be moved: ' . $yeniAd);
http_response_code(500);
echo 'The file could not be saved.';
return;
}
chmod($hedefKlasor . '/' . $yeniAd, 0644);
// The original name is kept in the database for display only
$sorgu = $pdo->prepare('INSERT INTO dosyalar (kullanici_id, disk_adi, ozgun_ad, tur) VALUES (?, ?, ?, ?)');
$sorgu->execute([$kullaniciId, $yeniAd, mb_substr(basename($dosya['name']), 0, 150), $tur]);Further recommendations:
- Re-encode images: Opening the uploaded image with GD or Imagick and saving it again confirms that the file really is an image and discards any extras embedded in it.
- Be careful with SVG: SVG files can contain scripts. Do not allow them unless you need to; if you do, sanitise them and serve them as downloads.
- Do not unpack archives: Automatically unpacking archives such as zip files on the server brings additional risks because of the file names and sizes inside them.
- Office and PDF documents: They do not run on the server, but they can carry a risk for the person who downloads them; run them through a malware scan if possible.
Serving a file from outside the web root
If the files are outside the web root, a small script that checks authorisation serves them. The script finds the file from the record in the database; it does not take a file path from the user.
<?php
// Requested by file ID; no path is taken from the user
$sorgu = $pdo->prepare('SELECT disk_adi, ozgun_ad, tur FROM dosyalar WHERE id = ? AND kullanici_id = ?');
$sorgu->execute([(int) ($_GET['id'] ?? 0), $kullaniciId]);
$kayit = $sorgu->fetch();
$yol = $kayit ? dirname(__DIR__) . '/depo/' . basename($kayit['disk_adi']) : '';
if (!$kayit || !is_file($yol)) {
http_response_code(404);
echo 'File not found.';
return;
}
header('Content-Type: ' . $kayit['tur']);
header('X-Content-Type-Options: nosniff');
header('Content-Disposition: attachment; filename="' . rawurlencode($kayit['ozgun_ad']) . '"');
header('Content-Length: ' . filesize($yol));
readfile($yol);The X-Content-Type-Options: nosniff header prevents the browser from interpreting the file as a different type from the one you declared. Where possible, serve files uploaded by users as an attachment (a download).
Disable script execution in the upload folder
If the files sit inside the web root, put an .htaccess like the one below in the upload folder. This block denies all access to files with PHP extensions in the folder; even if a script is somehow placed there, it cannot be called by URL.
# upload/.htaccess (inside the upload folder)
# Deny access to files with PHP extensions
<FilesMatch "(?i)\.(php[0-9]?|phtml|phar|pht)$">
Require all denied
</FilesMatch>
# Also deny names containing .php as an intermediate extension (e.g. name.php.jpg)
<FilesMatch "(?i)\.php[0-9]?\.">
Require all denied
</FilesMatch>
# Directory listing and CGI execution off
Options -Indexes -ExecCGI- This setting is for Apache 2.4 and hosting that allows the use of
.htaccess. If your server ignores.htaccessfiles (or you use Nginx), the same rule must be defined in the server configuration. - Test the rule: put a file with a
.phpextension that only prints a harmless piece of text in the folder and open its address; you should receive a "403 Forbidden" response. Then delete the file. - Your upload script must not allow a file named
.htaccessto be uploaded; the extension allowlist already prevents this. - The upload folder and the files in it should not have execute permission; 755 for the folder and 644 for the files is sufficient.
Signs of a web shell
When a backdoor has been planted on the server, the site usually carries on working normally. Watch out for these signs:
- Unexpected PHP files:
.phpfiles in upload, image, cache or temporary folders; names you do not recognise, random names or names made to look like system files. - Changed dates: Core files,
index.php,.htaccessor the configuration file modified on a date when you made no update. - File content: Sections that have nothing to do with normal code, consisting of very long single lines of meaningless character strings; unfamiliar code added at the start or end of a file.
- Strange redirects: Visitors who arrive from a search engine or on a phone being redirected to other sites; links and pages you did not add.
- Accounts and tasks: Administrator accounts, FTP accounts or scheduled tasks (cron) you do not recognise.
- Logs: Repeated POST requests to a single unknown PHP file; requests made to a file in the upload folder.
- External warnings: An abuse notice from your hosting provider, a security issue warning in Google Search Console, spam emails sent from your site.
Signature-based scanners help but are not enough on their own; they can report a backdoor they do not recognise as "clean". The most reliable method is to compare the files on the server with a clean source: every file and every difference that is not in the software's original package or in your own source code must be examined.
If you have found a malicious file
- Do not delete it straight away; copy it first. Keep a copy of the file, its modification date and the related log entries; you will need them to find the entry point.
- Do not assume it is the only file. Backdoors are usually left in more than one place. Compare all files with a clean source.
- Change all passwords: hosting panel, FTP/SFTP, database, admin panel.
- Find the entry point: Look at the log entries close to the time the file was created; which upload form, which plugin or which account was used?
- Close the vulnerability and update the software. If you only delete the file, it will be uploaded again by the same route.
For step-by-step incident response, see the What to do if your website is hacked guide.
Checklist
- The file type is verified from the content on the server; only types on the allowlist are accepted.
- The file name is generated randomly on the server; the name supplied by the user is not used on disk.
- There are limits on size and number of files.
- Files are outside the web root or in a folder where script execution is disabled.
- The rule in the upload folder has been tested (a PHP file returns 403).
- The upload form is protected by login and CSRF.
- Upload folders are checked regularly for unexpected files.
- Files can be compared with a clean source (there is an up-to-date copy of the source code or a list of file hashes).
Frequently asked questions
Why is checking only the extension not enough?
The extension is part of the file name and is set by the user; the content may be of a different type. Verifying the type from the content, generating the name on the server and disabling execution in the folder should all be applied together.
Is it safer to store files in the database?
It removes the execution risk, but it makes the database larger and backups heavier. A folder outside the web root provides the same security at lower cost in most cases.
The security scan said clean; can I be sure there is no web shell?
No. Scanners look for known signatures; they can miss a modified or new backdoor. Comparing the files with a clean source and examining unexpected files by name and location is more reliable.
I have no upload feature; is there still a risk?
Yes. A web shell can also be planted through a vulnerability in an outdated plugin or a compromised FTP account. Updates, strong passwords and file change monitoring are still necessary.
BYK Yazılım Support Team
This guide is written and regularly reviewed by the BYK Yazılım support team. Last updated: 4 October 2026.
Related guides
- What to do if your website is hackedStep-by-step incident response: maintenance mode, evidence, passwords, clean-up, entry point, restore and notifications.
- Server and hosting securitySFTP/FTPS, file permissions, directory listing, error display, sensitive files and database access.
- Website backup and monitoringFile and database backups, restore drills, file integrity, logs and uptime monitoring.
- What is XSS and how do you prevent it?Context-aware output escaping, Content-Security-Policy, HttpOnly cookies and rich-text sanitising.
Let us review your website together
BYK Yazılım builds corporate websites. Write to us with any questions about your site.
Contact us Our corporate website service