Jump to main content
University Computer Centre
Secure Coding With PHP

Secure Coding With PHP

General Information

The central web servers of the TU Chemnitz www.tu-chemnitz.de and www-user.tu-chemnitz.de support the scripting language PHP. The PHP commands integrated in documents are processed on the web server.

Security aspects play an increasingly important role in PHP programming, because

  • our servers are being used by a large number of web authors,
  • security gaps are exploited and abused frighteningly quickly,
  • our servers contain data worthy of protection that should only be made accessible to authorised persons.

It is not possible for us to achieve total security on the general web servers. Sensitive data may therefore only be transferred via suitable dedicated servers (e. g. via an own virtual server) and be made accessible through appropriate encryption procedures. But even then, it is essential to code as accurately as possible!

In the following tabs, some notes for secure programming are given by means of examples. Poorly programmed examples are marked red and corrected examples are green bordered.

Checking Variable Values

Do not trust any values that enter the PHP script via browser input, the URL or cookies! All external parameters, even if they come from hidden fields <input type="hidden" ... /> or selection menus, must be subjected to a plausibility check before they are used in the program. This affects all values of the fields _GET, _POST, _COOKIE, as well as _REQUEST. PHP offers a variety of possibilities for this.

# Value from form input field "number" must be numeric:
$number = (is_numeric($_REQUEST['number']) ? $_REQUEST['number'] : 0);

PHP has data filtering functions, that should be used for all external values:

# mini form
<form action="phpfilter.html" method="POST">
  Text: <input type="text" name="text">
  E-mail: <input type="text" name="mail">
  Number: <input type="text" name="number">
  <input type="submit">
</form>
<?php # Evaluation: Harmful characters are removed:
$text filter_input(INPUT_POST'text'FILTER_SANITIZE_STRING);
$mail filter_input(INPUT_POST'mail'FILTER_SANITIZE_EMAIL);
$number filter_input(INPUT_POST'number'FILTER_SANITIZE_NUMBER_INT);
echo <<<END
Eingaben nach der Filterung:
<pre>
text = $text
mail = $mail
number = $number
</pre>
END;

See also: Checking e-mail addresses

For more examples see http://www.php-faq.de/q/q-security-variablen.html (German only)

Avoid including additional scripts and files via variables, especially if they consist of components of the URL or cookies. This way, no unwanted data paths can be created by changing the variable name. Should this nevertheless be necessary necessary, take security precautions. Consider the following bad example:

# file is part of the URL
include($_REQUEST['file']);

Unwanted effects occur when the URL looks like this (e.g: dangerous executable files can be attached by changing the variable name): .../script.php?file=/etc/passwd

How is it done better? If a variable may only contain a file name in the same directory as the PHP script (but not a pathname), then it is often sufficient to check for / in the filename:

if (isset($_REQUEST['file']) && strstr($_REQUEST['file'], '/') === FALSE) {
   # file is set and does not contain /
   include($_REQUEST['file']);
} else {
   # error ...
}

If variables are only allowed to take values from a given set, (e.g. from a <select> menu), this should be checked:

# only these three values are valid:
$pages = array('index.html' => 1'contact.html' => 1,
               'teaching.html' => 1);
if (isset($_REQUEST['file']) && isset($pages[$_REQUEST['file']])) {
   # file is set and valid
   include($_REQUEST['file']);
} else {
   # error ...
}

You should also thoroughly check the parameters of functions for starting external programs like system(), exec(), passthru(), popen() and the `backtick` operator. To mask shell metacharacters (e.g. asterisk or semicolon, which possibly have undesirable effects) the function escapeshellarg() is suitable:

if (isset($_REQUEST['file']) && strstr($_REQUEST['file'], '/') === FALSE) {
   # file is set and does not contain /
   # possibly mask shell metacharacters:
   $file escapeshellarg($_REQUEST['file']);
   system("/bin/ls -l $file");
} else {
   # error ...
}

Further notes: http://www.php-faq.de/ch/ch-security.html (German only)

Cross Site Scripting (XSS)

Cross-site scripting means the "infiltration" of foreign HTML code onto a website. An attacker can use this to pursue the following goals:

  • Incorrect display of the website: Defacement (e.g. making text unreadable), display of third-party content (texts, images)
  • Spying on data, e.g. access data (esp. cookies), by inserting JavaScript code
  • Execution of third-party program code (esp. JavaScript)

In principle, all PHP and CGI programs that process external parameters (such as form inputs) are affected by such XSS problems, e. g. Guest books, search or registration forms.

Let us first examine a bad example:

<form action="…"><input name="name"  ></form>
   <?php  # Insert the typed name:
      print "Hello " $_REQUEST['name'];
   ?>

Unwanted effects occur when the URL looks like this: .../script.php?name=<h1>Upper-Case-And-Bolt

Then the PHP statement would output:

   Hello <h1>Upper-Case-And-Bolt

The HTML tag <h1> would severely disrupt the output. Of course, even more "wild" HTML tags (e.g. loading external images) or even JavaScript code could be smuggled in by the attacker and processed in the victim's web browser.

Let us remember the mnemonic - it cannot be mentioned often enough:

Do not trust any values that enter the PHP script via browser input, the URL or cookies. All external parameters, even if they come from hidden fields or selection menus must be subjected to a plausibility check, before they can be used in the program.

So for our example we critically consider the value for name:

<form action="…"><input name="name" ></form>
   <?php  # Insert the typed name
          # We write HTML code with the function htmlspecialchars:
      echo "Hello " htmlspecialchars($_REQUEST['name']);
   ?>

In this case, if the name typed in contains HTML code, it will be made "harmless" by the function htmlspecialchars. In the above example, the output is:

   Hello &lt;h1&gt;Upper-Case-And-Bolt

And that does not mess up a web browser - even JavaScript is rendered harmless this way. Alternatively, you can use the function strip_tags, in order to remove all HTML and PHP tags from a string.

SQL Injection

PHP is an excellent tool for displaying or changing data from databases on websites. But here, too, security aspects must be taken into account to avoid unpleasant surprises.

"SQL injection" is a technique by which malicious attackers can create or modify existing SQL commands in order to make hidden data visible, modify it or delete it.

Let us look at a bad example again (all examples use PHP Data Objects – PDO) and an attack scenario:

<?php
     #Database query using an ID from the URL
     $id $_REQUEST['id'];
     $result $db->query("SELECT * from tabelle WHERE id=$id");
     # Display of results …
?>

An URL in the form .../script.php?id=42 is expected, which reads the table entry with the id=42 from the database. But what if the URL is written like this: .../script.php?id=42+or+1=1
The web server converts the plus signs into blanks, and the unsuspecting call $db->query() -suddenly leads to all (possibly sensitive) data being read out:
SELECT * from tabelle WHERE id=42 or 1=1

In this case, too, we remember the mnemonic: We have to check all input values, even if it takes a bit of effort. However, PHP has a lot of possibilities for this.

If an integer number is expected as the value, you can check this with the function ctype_digit() or you set the type explicitly with settype() to integer:

<?php
     #Database query using an ID from the URL
     $id $_REQUEST['id'];
     settype($id'integer');   # Forced conversion to number
     $result $db->query("SELECT * from tabelle WHERE id=$id");
     # Display of results ...
?>

Fortunately, functions of the PDO help us: Whenever possible, use the prepared statements (prefabricated queries) with placeholders. When replacing with values, PDO automatically ensures safe coding of the values.

<?php    # Full example with PDO
  if (! isset($_REQUEST['id'])) return;  # id not set: End

  try {
    $db = new PDO('mysql:host=www-db.tu-chemnitz.de;dbname=events;charset=utf8');
  } catch(PDOException $ex) {
    die('No DB connection');
  }

  # Preparation with placeholders
  $query $db->prepare("SELECT titel,DATE_FORMAT(startdate,'%d.%m.%Y') as date FROM event WHERE id=?");
  # Execute
  $query->execute(array($_REQUEST['id']));  # quite safe

  # Query results
  while ($r $query->fetch()) {
    echo 'Title: ' htmlspecialchars($r['title']) .
           ' on: ' htmlspecialchars($r['date']) . "<br />\n";
  }
?>

The programming care must be even greater when the database is also to be changed via PHP. INSERT- and UPDATE- operations must ensure that plausible values are inserted. Of course special care is needed with DELETE- operations etc. The prepared statements also provide support here.

<?php
# Set up DB connection omitted
# Check input values, e.g.
if (!isset($_POST['name'])) {
  die('No value specified for name!');
}
$name trim($_POST['name']);  # trim = remove spaces or similar at the beginning/end
$len strlen($name);
if ($len || $len 31) {
  die('Name must be between 3 and 31 characters long.');
}
# etc. for $first_name and $age

$ins $db->prepare('INSERT INTO table (name, first_name, age) VALUES (?,?,?)');
$res $ins->execute(array($name$first_name$age))
           or die('Error during insertion.');
?>

Before requesting data, the connection to the database must first be established. Server name, the database user and the password must be entered, e.g. for MySQL:

<?php
     # Establish database connection - we use PDO:
     $db = new PDO('mysql:host=mysql.hrz.tu-chemnitz.de;dbname=XYZ''user''secret password');
?>

At this point we have the problem that the password of the database user must stand in the PHP script. Of course, this poses a danger if it falls into the wrong hands. Please note this basic rule: Use a database user whose rights are limited according to your application. If you only execute SELECT statements via PHP script, use the DB user who has read-only rights.

This special proceeding on the central web servers of TU Chemnitz, provides web authors a secure way to use such secrets in web applications without having to store them in the accessible file system.

For applications and projects with higher security requirements dedicated web servers should be provided. The URZ therefor provides virtual private servers.

Futher notes:

E-mail Dispatch

E-mails can be sent with the help of PHP. In this case great care is needed when coding, because carelessly programmed scripts are often and frighteningly quickly misused by spammers. We have had to suffer the consequences several times: Our mail servers end up on block lists, which hampers all users.

First look at another bad example. A form is used to request an e-mail address, which is then sent to a mailing list server:

<form action="..." method="post">
E-mail address: <input name="address" /> <input type="submit" value="fill" />
</form>

<?php
   if (isset $_POST['address'] && $_POST['addsress'] != '') {   # Form input set?
      mail('test-join@tu-chemnitz.de''''''From: ' $_POST['address']);
   }
?>

The attentive reader will immediately notice that the variable $_POST['address'], which comes from the browser (or the attacker), is adopted almost unchecked. A fatal mistake, because this is exact the type of weak point that gets attacked.

Have a look at the arguments of the PHP function mail(): The first three arguments are clear: Each one are strings for recipient, subject and content text. (in our example, subject and content are empty). As a fourth argument, a string with further headers of the e-mail can be specified. In the example, the sender address (From:) is to be set. If $_POST['address'] contains a valid e-mail address, all is well.

An attacker can, however, send us completely different data as supposed form data, such as the following: whatever%0ABcc:%20poor@spam.victim,anotherone@spam.victim%0A%0AClick-buy

Decoding this (%0A is line break) results in the following string, which are inserted into the mail as headers:

  From: whatever
  Bcc: poor@spam.victim,anotherone@spam.victim

  Click-buy

The result: The e-mail is also sent to the "...@spam.victim" addresses, even with content, because the two line breaks mean the end of the headers. Of course, the PHP function mail() should check and prevent this, but unfortunately it does not.

As PHP programmers, we are of course obliged to carefully check all external data that enters our program. I repeat myself, don't I? This is how it could look for the above example:

<form action="..." method="post">
E-mail-address: <input name="address /> <input type="submit" value="fill" />
</form>

<?php
   if (isset $_POST['address'] && $_POST['address'] != '') {   # Form input set?
      if (preg_match("/[\r\n]/"$_POST['address']) {
          # address contains line break: one of the characters \r or \n
          # -> not with us - we reject that!
          exit;
      }
      mail('test-join@tu-chemnitz.de''''''From: ' $_POST['address']);
   }
?>

There are other "traps" when sending an e-mail. For example, the sender address should be set correctly so that error mails arrive correctly (and do not go to the operator of the WWW server). In addition, the character sets or encodings used must be set.

In order to relieve a PHP programmer of this task, we provide a separate e-mail function for the central WWW servers of the TU Chemnitz, tuc_mail(), which you should use. To illustrate this, take the following example:

<?php
require_once('php/mail.inc');    # specify tuc_mail()

# One recipient
  $to 'John Doe <john.doe@hrz.tu-chemnitz.de>';
# For multiple recipients: array() -> is sent as Bcc
# $to = array('jane.doe@hrz.tu-chemnitz.de', 'john.doe@hrz.tu-chemnitz.de');

# Sender, must be from *.tu-chemnitz.de!
  $from 'Frank Richter <frank.richter@hrz.tu-chemnitz.de>';

# Subject, possible: Umlauts in UTF-8 or Latin1- (ISO-8859-1)
  $subject 'PHP test: äöü';

# Content, here are umlauts also possible
  $text 'This is a test message triggered by ' .
          $_SERVER['REMOTE_ADDR'];
# reply to - this is not set:
  $reply_to '';

  $ok tuc_mail($to$from$subject$text$reply_to);

  if ($ok === TRUE)
      echo "E-mail has been sent.";
  else
      echo "Error during sending: " htmlspecialchars($ok);

?>

The function tuc_mail() requires five arguments - strings for each recipient, sender, subject, content, and if desired, reply address. If the sending worked, the function returns TRUE, in case of an error a string with the cause of the error is returned.

Our function first checks the security of the arguments, sets the sender address, recognises and marks the character set of the subject and content (Latin-1 or UTF-8). So it is almost an "all-round carefree package" for free. Please use it!

File Upload

PHP offers functions that make it quite easy to upload files from the web browser to the web server. You can find an explanation of the necessary HTML form and the PHP instructions in the PHP documentation.

Following you can see a critical excerpt - a rudimentary HTML form and the duplicating of an uploaded file from the temporary area into the designated directory:

<form enctype="multipart/form-data" action="..." method="post">
  <input name="file" type="file" />
  <input type="submit" value="upload file" />
</form>

<?php
$upload_directory '/afs/tu-chemnitz.de/.../upload';

# Name for upload element in the form is 'file'
if (isset($_FILES['file']['name'])) {
    $file_name $_FILES['file']['name'];
# Check file names: Only letters, full stops, underscores and hyphens allowed:
  if (preg_match('/^[a-zA-Z0-9._-]*$/'$file_name)) {

  # IMPORTANT: Check if files already exists to prevent overwriting!
    if (file_exists("$upload_directory/$filename")) {
      echo "File " htmlspecialchars($file_name) . " already exists!";
    } else {
      if (move_uploaded_file($_FILES['file']['tmp_name'],
                             "$upload_directory/$file_name")) {
        echo "Ok";
      } else {
        echo "Error: " $_FILES['file']['error'];
      }
    }
  } else {
    echo "Error: Invalid file name " htmlspecialchars($file_name);
  }
}
?>

Despite these precautions, such a procedure offers a "gateway" through which undesirable files, such as malware, can enter our system. Therefore, thorough considerations and careful progamming with regards to security are required.

If possible, do not offer such an upload option publicly for everyone, but only allow it via authentication for authorised persons. You can find notes on access protection via instructions in the file .htaccess on this page Apache: Access control.

Special attention must be paid to the directory into which the uploaded file is to be written (this also applies to non-public access to the upload script!). Risks occur, since this directory must be writable for the web server in question:

  • Stored files may contain malware (e.g. viruses).
  • In principle, any program running on the web server can write data to this directory (or delete and change it if it has the rights to do so). This can happen unintentionally, but also maliciously through malware.
  • If stored "hacker scripts" (PHP or CGI) are executed by the web server, further damage can occur (e.g. data manipulation).

Due to these risks, special care is needed for writeable directories:

  • Use a separate directory that does not contain your own files.
  • Only set the absolutely necessary AFS rights, e.g. only allow the web server www-user.tu-chemnitz.de to list and insert new files (but not to read or overwrite them):
    ip:www-user li
    see also Access protection to web documents in the AFS file system
  • Restrict read access via the web server to this directory by means of .htaccess!

You must also ensure that files in this upload directory are not directly readable though the web browser:

  • It is best to place the directory outside your web area (that means not under /afs/tu-chemnitz.de/www/root or $HOME/public_html)
  • Remove the AFS reading right for system:anyuser and the WWW server.
  • Or protect the directory with instructions in the file .htaccess
    # Prevent direct access to all files
    Require all denied
    
  • If you need to allow read access to the uploaded files (e.g. for the exchange of data among authorised persons), you must at least prevent the execution of CGI or PHP scripts. Then write in the .htaccess
    php_flag engine off
    RemoveHandler .cgi
    

The protective measures must be different depending on the intended use. Read further "Writeable directories: Risks and safeguards".

By the way: If you need an upload capability for your HOME directory (or for other directories for which you have write permission), you can fall back on a ready-made solution: Use the web-based file manager WFM of the login server: https://login.tu-chemnitz.de/wfm/