Programming on the Server Side
41 Avoiding Injection Attacks
In previous chapters, you were shown some best practices for avoiding malicious attacks on your app. The correct use of filter_input() and SQL query parameters will make your app much more secure than apps that receive parameters and construct SQL queries in other ways. This chapter compares these best practices to other, less good practices and motivates and explains the importance of doing things the right way.
Injection Attacks
Server-side PHP programs routinely receive input from users through HTTP Request parameters sent from HTML forms. They take this user input and incorporate it into web pages and SQL queries. In other words, user input is used to write code that gets executed on the client or server machine. Hackers can take advantage of this feature of server-side programming to launch a code injection attack. In a code injection attack, malicious code written by a hacker is sent to a server-side script, usually as an HTTP Request parameter, and the script then runs the code as part of a web page or SQL query.
Cross Site Scripting (XSS)
When an injection attack involves executing JavaScript code on a web page, it is often referred to as a Cross-Site Scripting (XSS) attack.
Do it Yourself
To see a simple, and relatively harmless, Cross-Site Scripting (XSS) attack in action, get the file hackMe.php from the Full Stack Example Pack and run it via a server. This app receives an HTTP Request parameter called param and echoes it to the page without using filter_input to sanitize it first.
To see why this is a problem, try the GET parameter show below in the URL:
hackMe.php?param=<script>location.reload()</script>
This benign hack causes a JavaScript command to be injected into the HTML code for the page. The command refreshes the page as soon as it’s loaded, effectively disabling it and wasting browser cycles and bandwidth.
To see the fix, get the file hackMeFixed.php from the Full Stack Example Pack and run it via a server. Try the same attack and see what happens, then check the code to look for the difference between the two files.
The Problem: $_GET
The hackMe.php code from the Do it Yourself box above is accessing the get parameter using a method that has not yet been officially introduced in this book. Here’s what it looks like:
echo "<p>$_GET[param]</p>";
The above statement is retrieving the GET parameter from a superglobal associative array called $_GET instead of using filter_input(). The superglobal $_POST is also available for post parameters and $_REQUEST contains all GET and POST parameters together as well as any cookies received. These variables are the oldest and most established ways of receiving parameters in PHP, but these days many PHP programmers would recommend that you never use them.
The Solution: Filtering
The fix for the code injection problem above is easy: sanitize the parameter using filter_input() before placing it on the page (this is the fix implemented in hackMeFixed.php). There are other ways to sanitize and validate your parameters but filter_input() is considered a best practice solution by many programmers because it allows you to avoid using $_GET and $_POST anywhere in your code. If you make it a policy to never access $_GET and $_POST directly, then it is much less likely that you will accidentally expose yourself to a code injection attack.
Filtering and Run-time Errors
Making sure you always use filter_input() with the correct filter type and checking its return value will also insulate your code from certain types of run-time errors. Run-time errors are to be avoided because they can lead to a negative user experience, but they can also sometimes tip off a hacker about other vulnerabilities in your system. For example, if your server is configured to display PHP error messages on the page (the default configuration for XAMPP) and you access the $_POST array looking for a userid parameter, the user might see something like the message below.
Notice: Undefined index: userid in https:\\example.com\library\utilities.php on line 2
This error exposes:
- The language this part of the app is written in,
- The name of the parameter it wants to receive,
- The URL of one of the PHP files involved in the app (which might not have been visible if it was loaded using an include statement).
All of this information could be used by a hacker to figure out a way to gain unauthorized access to your app or your user data. For this reason, most production servers turn off error reporting entirely. Error reporting is used only during development, and only on internal development servers. Still, any server can be accidentally misconfigured, so you should always avoid any possibility of run-time errors to avoid exposing vulnerabilities to a malicious user.
SQL Injection
In this book, you were shown how to use PDO to prepare and execute statements with query parameters (i.e. question marks) that get filled in, often using HTTP Request Parameters, when you execute. But a simpler and very dangerous practice would be to simply concatenate data from the user directly into the command string.
Here’s an example:
$id = filter_input(INPUT_POST, "id", FILTER_SANITIZE_SPECIAL_CHARS); $deletecommand = "DELETEFROM MyTable WHERE ID=$id"; $stmt = $dbh->prepare($deletecommand); $stmt->execute();
Even though the value assigned to $id is filtered, it is still not safe to simply concatenate it into the $deletecommand string. The code above is vulnerable to an SQL Injection attack in which a hacker types some SQL syntax into a form which then gets concatenated (i.e. injected) into the SQL command string, potentially wreaking havoc on the database.
A simple attack on the code above would be for the user to send an id parameter set to something like “1 OR 1”. In that case the command string becomes
DELETE FROM MyTable WHERE ID=1 OR 1;
Since 1 is interpreted as TRUE in SQL, the above is equivalent to:
DELETE FROM MyTable WHERE (ID=1) OR (TRUE);
The WHERE clause evaluates to TRUE for every row, so every row in the table gets deleted.
Using other, cleverly constructed, SQL Injection attacks, a hacker might be able to retrieve passwords or other sensitive data, login as an administrator, etc. The SQL Injection section of W3Schools has a few other examples of possible injection attacks.
Do it Yourself
You can try out the example above using the sql_injection example from the Full Stack Example Pack.
- Import newtesttable.sql using phpMyAdmin. Browse the table.
- Modify the connect.php script to match your database and login credentials.
- Load index.html and use the form to delete one row. Verify using phpMyAdmin.
- Load index.html again, and this time use “1 or 1” as the delete ID. This will delete all the rows! Verify that the attack worked in phpMyAdmin.
If you change index.html so that the form directs to deleteSafe.php instead of delete.php, the above attack will not work. The deleteSafe.php version of the code uses a prepared statement parameter (as explained in the PHP Data Objects chapter), and then sends the data to fill in the parameter as an argument to the execute method.
How Do Parameters Help?
One way to think of the difference between using prepared statement parameters and just concatenating the command string together is that the prepare method compiles the command into a function which accepts an argument and executes the command using that argument.
Let’s call the function created by the prepare method in the above example deleteById. Then if $id contains the string “1 OR 1” the execute statement in the above example calls deleteById like this:
deleteById(“1 OR 1”);
The function goes to the database looking for rows with id set to “1 OR 1”. Since there are no such rows, the function deletes nothing. The parameter helped because input from the user was treated as data rather than being interpreted as code.
This SQL Injection example could also have been neutralized using FILTER_VALIDATE_INT instead of FILTER_SANITIZE_SPECIAL_CHARACTERS because “1 OR 1” is not an integer and would have caused filter_input to return false. To be as safe as possible, it is a best practice to use the correct input filter and also use parameters in the SQL query string as well. If multiple lines of defense are always in place, then all the code you write will be safer.
Client Side Form Validation
The story of this chapter is that you must validate and sanitize all the data coming into your server, and you must use prepared statement parameters for SQL so that your code is not vulnerable to injection attacks and other possible hacks. But what if we carefully construct HTML forms and use HTML attributes and JavaScript event listeners to validate the forms and prevent the user from submitting invalid data in the first place?
For example, an HTML form could:
- use the required attribute on an <input> element to prevent empty parameters,
- use type=”number”, type=”email”, etc. to force a parameter into the proper format,
- use a <select> element to force the user to pick from a list of valid choices,
- use JavaScript event listeners on the submit, input, or change events can to catch other issues, even as the user types,
Using all the above tricks and more, you can constrain the user quite effectively. But unfortunately, client-side validation is not enough to make your app safe from attack. The server has no way of knowing where an HTTP Request is coming from or how it was created. A hacker can enter the browser’s developer tools to turn off all the client-side validation, or create their own HTTP Request using a form or script written for that purpose.
But client-side validation is still important for other reasons.
- It creates a nicer user experience. Users will not have to wait for a page load to find out they have submitted bad data. And with JavaScript, you can alert them to problems using feedback as they fill in the form.
- It minimizes wasted resources by preventing HTTP Requests containing invalid data. Every bad HTTP Request wastes bandwidth and processing time on the server, which has to create the HTTP Response, run scripts, and possibly execute database queries for the invalid request.
The bottom line is that for security, conservation of resources, and a better user experience, you must validate user input on both the server-side and the client-side of your app.