How to secure PHP web applications and prevent attacks?

1) Cross site scripting (XSS)

XSS attacks happen when client-side code (usually JavaScript) gets injected into the output of your PHP script. This can be through the URL, but can also occur via a stored technique such as the database.

// GET data is sent through URL: http://example.com/search.php?search=<script>alert('test')</script>
$search = $_GET['search'] ?? null;
echo 'Search results for '.$search;

// This can be solved with htmlspecialchars
$search = htmlspecialchars($search, ENT_QUOTES, 'UTF-8');
echo 'Search results for '.$search;
  • ENT_QUOTES is used to escape single and double quotes beside HTML entities
  • UTF-8 is used for pre PHP 5.4 environments (now it is default). In some browsers some characters might get pass the htmlspecialchars().

2) SQL injection

When accessing databases from your application, SQL injection attack can happen by injecting malicious SQL parts into your existing SQL statement.

SQL injection

When working with databases, one of the most common security vulnerabilities in web applications is definitely SQL injection attacks. Malicious users can insert SQL queries into inputs handled by code that interacts with datbases in order to cause unwanted behavior.

SQL injection example with PDO

// GET data is sent through URL: http://example.com/get-user.php?id=1 OR id=2;
$id = $_GET['id'] ?? null;

// You are executing your application as usual
// Connect to a database
$dbh = new PDO('mysql:dbname=testdb;host=127.0.0.1', 'dbusername', 'dbpassword');

// Select user based on the above ID
// bump! Here SQL code GET data gets injected in your query. Be careful to avoid
// such coding and use prepared statements instead
$sql = "SELECT username, email FROM users WHERE id = " . $id;

foreach ($dbh->query($sql) as $row) {
    printf ("%s (%s)\n", $row['username'], $row['email']);
}

Just imagine worst case scenarios with injected SQL:

"'DELETE FROM users */"

How to avoid SQL injection as per the example above? Use prepared statements:

$sql = "SELECT username, email FROM users WHERE id = :id";

$sth = $dbh->prepare($sql, [PDO::ATTR_CURSOR => PDO::CURSOR_FWDONLY]);
$sth->execute([':id' => $id]);
$users = $sth->fetchAll();

mysqli example

When using a MySQL database, you can also use mysqli with prepared statements, or the mysqli_real_escape_string() function, but you can also just use PDO instead.

// get data is sent through url for example, http://example.com/get-user.php?id=1 OR id=2;
$id = $_GET['id'] ?? null;

// in your code you are executing your application as usual
$mysqli = new mysqli('localhost', 'db_user', 'db_password', 'db_name');

if ($mysqli->connect_error) {
    die('Connect Error (' . $mysqli->connect_errno . ') ' . $mysqli->connect_error);
}

// bump! sql injected code gets inserted here. Be careful to avoid such coding
// and use prepared statements instead
$query = "SELECT username, email FROM users WHERE id = " . $id;

if ($result = $mysqli->query($query)) {
    // fetch object array
    while ($row = $result->fetch_row()) {
        printf ("%s (%s)\n", $row[0], $row[1]);
    }

    // free result set
    $result->close();
} else {
    die($mysqli->error);
}

Let’s fix this with prepared statements. They are more convenient becausemysqli_real_escape_string() doesn’t apply quotes (it only escapes it).

// Get data is sent through url for example, http://example.com/get-user.php?id=1 OR id=2;
$id = $_GET['id'] ?? null;

// In your code you are executing your application as usual
$mysqli = new mysqli('localhost', 'db_user', 'db_password', 'db_name');

if ($mysqli->connect_error) {
    die('Connect Error (' . $mysqli->connect_errno . ') ' . $mysqli->connect_error);
}

// bump! sql injected code gets inserted here. Be careful to avoid such coding
// and use prepared statements instead
$query = "SELECT username, email FROM users WHERE id = ?";

$stmt = $mysqli->stmt_init();

if ($stmt->prepare($query)) {
    $stmt->bind_param("i", $id);
    $stmt->execute();
    $result = $stmt->get_result();
    while ($row = $result->fetch_array(MYSQLI_NUM)) {
        printf ("%s (%s)\n", $row[0], $row[1]);
    }
}

3) Directory traversal (path injection)

Directory traversal attacks, also known as ../ (dot, dot, slash) attacks, happen when users supply filenames as input that can traverse to parent directories. Data can be set as index.php?page=../secret, or /var/www/secret, or something more catastrophic:

$page = $_GET['page'] ?? 'home';

require $page;
// or something like this
echo file_get_contents('../pages/'.$page.'.php');

In such cases you must check if there are attempts to access the parent or some remote folder:

// Checking if the string contains parent directory
if (strstr($_GET['page'], '../') !== false) {
    throw new \Exception("Directory traversal attempt!");
}

// Checking remote file inclusions
if (strstr($_GET['page'], 'file://') !== false) {
    throw new \Exception("Remote file inclusion attempt!");
}

// Using whitelists of pages that are allowed to be included in the first place
$allowed = ['home', 'blog', 'gallery', 'catalog'];
$page = (in_array($page, $allowed)) ? $page : 'home';
echo file_get_contents('../pages/'.$page.'.php');

Command injection

Be careful when dealing with commands executing functions and data you don’t trust.

exec('rm -rf '.$GET['path']);

Code injection

Code injection happens when malicious code can be injected via the eval() function, so remember to always sanitize your data when using it:

eval('include '.$_GET['path']);

Cross site request forgery (XSRF/CSRF)

Cross site request forgery, one click attacks, or session riding is an exploit whereby users execute unwanted actions on web applications.

Public files

Make sure to move all your application files, configuration files and similar parts of your web application to a folder that isn’t publicly accessible when you visit URLs of your web application. Some types of files (e.g., .yml files) might not be processed by your web server and users could view them online.

An example of good folder structure:

app/
  config/
    parameters.yml
  src/
public/
  index.php
  style.css
  javascript.js
  logo.png

Configure your web server to serve files from the public folder instead of from your application root folder. The public folder contains the front controller (index.php). In case of a web server misconfiguration resulting in PHP files failing to be served properly, the source code of index.php will be visible to the public.

Passwords

When working with users’ passwords, hash them properly with the password_hash()function.

Uploading files

Many security breaches occur when users can upload files onto a server. Make sure you go through all the vulnerabilities associated with uploading files and take appropriate precautions against these vulnerabilities, such as by renaming uploaded files, moving them to publicly inaccessible folders, checking the types of files uploaded and so on. Since there are many issues to check here, more information is also located in the separate FAQ.

The HTML form for uploading single or multiple files must include theenctype="multipart/form-data" attribute. Use the POST method:

<form method="post" enctype="multipart/form-data" action="upload.php">
    File: <input type="file" name="pictures[]" multiple="true">
    <input type="submit">
</form>

And the PHP upload.php script looks like the following:

<?php

foreach ($_FILES['pictures']['error'] as $key => $error) {
    if ($error == UPLOAD_ERR_OK) {
        $tmpName = $_FILES['pictures']['tmp_name'][$key];
        // basename() may prevent directory traversal attacks, but further
        // validations are required
        $name = basename($_FILES['pictures']['name'][$key]);
        move_uploaded_file($tmpName, "/var/www/project/uploads/$name");
    }
}

Don’t stop here just yet and continue reading! The uploaded files must be validated for security purposes. A lot of hacks can occur when uploading hasn’t been properly secured. Imagine a malicious attacker uploads evil.php which is publicly accessible over https://example.com/uploads/evil.php!

Validation

Always make sure that you implement server-side validation in order to be able to upload securely, and make sure that you understand the reasons for this, and the security vulnerabilities that you would otherwise be exposed to.

Directory traversal

To avoid directory traversal (a.k.a. path traversal) attacks, use basename() like shown above, or even better, rename the file completely like in the next step.

Rename uploaded files

Renaming uploaded files avoids duplicate names in your upload destination, and also helps to prevent directory traversal attacks. If you need to keep the original filename, you can it in a database for retrieval in the future. As an example, renaming a file with microtime() and some random number:

$uploadedName = $_FILES['upload']['name'];
$ext = strtolower(substr($uploadedName, strripos($uploadedName, '.')+1));

$filename = round(microtime(true)).mt_rand().'.'.$ext;

You can also use hashing functions like hash_file() and sha1_file() to build filenames. This method can save some storage space when different users upload the same file.

$uploadedName = $_FILES['upload']['name'];
$ext = strtolower(substr($uploadedName, strripos($uploadedName, '.')+1));

$filename = hash_file('sha256', $uploadedName) . '.' . $ext;

Check file type

Instead of relying on file extensions, you can get the mime-type of a file with finfo_file():

$finfo = finfo_open(FILEINFO_MIME_TYPE); // return mime-type extension
echo finfo_file($finfo, $filename);
finfo_close($finfo);

For images, a check that’s more reliable, but still not really good enough is using the getimagesize() function:

$size = @getimagesize($filename);
if (empty($size) || ($size[0] === 0) || ($size[1] === 0)) {
    throw new \Exception('Image size is not set.');
}

Check file size

To limit or check the size of the uploaded file, you can check $_FILES['files']['size']and the errors UPLOAD_ERR_INI_SIZE and UPLOAD_ERR_FORM_SIZE:

if ($_FILES['pictures']['size'] > 1000000) {
    throw new RuntimeException('Exceeded filesize limit.');
}

Storing uploads to a private location

Instead of saving uploaded files to a public location available athttps://example.com/uploads, storing them in a publicly inaccessible folder is a good practice. To deliver these files, so called proxy scripts are used.

Client-side validation

For better user experience, HTML offers the accept attribute to limit filetypes by the extension or mime-type in the HTML, so users can see the validation errors on the fly and select only allowed filetypes in their browser. However, browser support is limitedat the time of writing this. Keep in mind that client-side validation can be easily bypassed by hackers. The server-side validation steps explained above are more important forms of validation to use.

Full example

Let’s take all of the above into consideration and look at a very simple example:

// Check if we've uploaded a file
if (!empty($_FILES['upload']) && $_FILES['upload']['error'] == UPLOAD_ERR_OK) {
    // Be sure we're dealing with an upload
    if (is_uploaded_file($_FILES['upload']['tmp_name']) === false) {
        throw new \Exception('Error on upload: Invalid file definition');
    }

    // Rename the uploaded file
    $uploadName = $_FILES['upload']['name'];
    $ext = strtolower(substr($uploadName, strripos($uploadName, '.')+1));
    $filename = round(microtime(true)).mt_rand().'.'.$ext;

    move_uploaded_file($_FILES['upload']['tmp_name'], __DIR__.'../uploads/'.$filename);
    // Insert it into our tracking along with the original name
}

Server configuration

The server-side validation mentioned above can be still bypassed by embedding custom code inside the image itself with tools like jhead, and the file might be ran and interpreted as PHP.

That’s why enforcing filetypes should also be done at the server level.

Apache

Make sure Apache is not configured to interpret multiple files as the same (e.g., images being interpreted as PHP files). Use the ForceType directive to force the type on the uploaded files.

<FilesMatch "\.(?i:pdf)$">
    ForceType application/octet-stream
    Header set Content-Disposition attachment
</FilesMatch>

Or in the case of images:

ForceType application/octet-stream
<FilesMatch "(?i).jpe?g$">
    ForceType image/jpeg
</FilesMatch>
<FilesMatch "(?i).gif$">
    ForceType image/gif
</FilesMatch>
<FilesMatch "(?i).png$">
    ForceType image/png
</FilesMatch>

Nginx

On Nginx, you can use the rewrite rules, or use the mime.types configuration file provided by default.

location ~* (.*\.pdf) {
    types { application/octet-stream .pdf; }
    default_type application/octet-stream;
}

Session hijacking

Session hijacking is an attack where an attacker steals the session ID of a user. The session ID is sent to the server where the associated $_SESSION array is populated. Session hijacking is possible through an XSS attack or when someone gains access to the folder on a server where the session data is stored.

Remote file inclusion

An RFI (remote file inclusion) attack is when an attacker can include custom scripts:

$page = $_GET['page'] ?? 'home'

require $page . '.php';

In the above code, $_GET can be set to a remote file http://yourdomain.tld/index.php?page=http://example.com/evilscript

Make sure you disable this in your php.ini unless you know what you’re doing:

; Disable including remote files
allow_url_fopen = off
; Disable opening remote files for include(), require() and include_once() functions.
; If above allow_url_fopen is disabled, allow_url_include is also disabled.
allow_url_include = off

PHP configuration

Error reporting

In your production environment, you must always turn off displaying errors to the screen. If errors occur in your application and they are visible to the outside world, an attacker could get valuable data for attacking your application. display_errors and log_errors directives in the php.ini file:

; Disable displaying errors to screen
display_errors = off
; Enable writing errors to server logs
log_errors = on

Exposing PHP version

PHP version is visible in HTML headers. You might want to consider hiding your PHP version by turning off the expose_php directive, preventing the web server from sending back the X-Powered-By header:

expose_php = off

Remote files

In most cases, it’s important to disable access to remote files:

; disabled opening remote files for fopen, fsockopen, file_get_contents and similar functions
allow_url_fopen =  0
; disabled including remote files for require, include ans similar functions
allow_url_include = 0

open_basedir

This settings defines one or more directories (subdirectories included) where PHP has access to read and write files. This includes file handling (fopenfile_get_contents) and also including files (includerequire):

open_basedir = "/var/www/test/uploads"

Session settings

  • session.use_cookies and session.use_only_cookiesPHP is by default configured to store session data on the server and a tracking cookie on client-side (usually called PHPSESSID) with unique ID for the session.
; in most cases you'll want to enable cookies for storing session
session.use_cookies = 1
; disabled changing session id through PHPSESSID parameter (e.g foo.php?PHPSESSID=<session id>)
session.use_only_cookies = 1
session.use_trans_sid = 0
; rejects any session ID from user that doesn't match current one and creates new one
session.use_strict_mode = 1
  • session.cookie_httponlyIf the attacker somehow manages to inject JavaScript code for stealing a user’s current cookies (the document.cookie string), the HttpOnly cookie you’ve set won’t show up in the list.
session.cookie_httponly = 1
  • session.cookie_domainThis sets the domain for which cookies apply. For wildcard domains you can use .example.com, or set this to the domain where it should be applied. By default, it isn’t enabled, so it’s highly recommended for you to enable it:
session.cookie_domain = example.com
  • session.cookie_secureFor HTTPS sites, this accepts only cookies sent over HTTPS. If you’re still not using HTTPS, you should consider it.
session.cookie_secure = 1

Use HTTPS

HTTPS is a protocol for securely communication over networks. It’s highly recommended that you enable it on all sites.

Please follow and like us:

Leave a Reply

Your email address will not be published. Required fields are marked *