Web applications scripted in PHP are vulnerable if proper precautions are not taken to ensure security. This article highlights the measures to be taken to reduce and eliminate these vulnerabilities, which generally arise due to negligent programming. An ounce of prevention is better than a pound of cure, the proverb says. For developers, no amount of caution is too much!
I’ve seen many people abandon PHP for security reasons. Many do this just because someone else told them it is not secure, but some have experienced the hard reality of attackers taking control of their websites and doing severe damage (even financially).
So, what’s the truth? Is PHP inherently vulnerable? Well, it is true that PHP has many pitfalls, but nobody will deny the fact that it is one of the most friendly Web development tools out there, and even some of the busiest websites on the planet (including Wikipedia) run on it. These have no particular issues that make them more vulnerable compared to sites developed in other languages.
As with most ‘bad things’, PHP’s bad reputation comes from how it is used. It is easy to write vulnerable apps in PHP, but it is easier to get cheated by the false sense of security induced by some languages and frameworks. They cover us as long as our app is a stock item, but the moment we do something out-of-the-box, we are on our own. The Web is one of the most dangerous environments for an application to run, where arrows can come from unexpected directions. You might have built the backend upon a super secure framework, but your false expectations of how the client-side works might cost you everything.
So instead of hopping from framework to framework trying to find the most secure one, let’s first try to understand and undo our mistakes. This article discusses some security considerations that are useful while building any Web app, but from a PHP viewpoint.
Injection attacks
Whenever a website processes and outputs some value from the client side, either as fresh entries or modifications of the defaults, there is a chance of attacks. The visitor can knowingly or unknowingly enter dangerous values, or a third party can conceive a malicious URL and make a legit client visit it. Such attacks fall into the injection and cross-site scripting (XSS) categories.
First, think about where the data comes from. There will be user-settable and user-editable values in $_GET, $_POST and $_FILES. Values in $_COOKIE are set by you, but can be edited by a malicious visitor. That means additional checks might be needed. Also, never set secrets and privileges in cookies. Store them in $_SESSION and the database (server side). Cookies are just to identify the user and to keep extremely temporary stuff.
Cross-site scripting
Cross-site scripting (XSS) attacks include various tricks by which attackers inject code into websites without accessing or modifying the server code. This means your users can be harmed while your server remains intact.
A simple example is your site accepting a piece of text from the user and displaying it back. If the visitor enters HTML instead of text, that gets rendered. If the visitor enters JavaScript, that gets executed. Isn’t this self-induced damage? No, it can affect other users (more on this in the ‘Session hijacking’ section).
The solution is to escape all specials in the user-input values before displaying (e.g., convert < to <). There are functions like htmlspecalchars() in PHP for this.
File uploading
In file uploading forms, you’ll have to add client-side size and type checks (usually using JavaScript) so that the client can see if the file is incompatible even before the upload starts. But be sure to add the same checks in the server-side PHP so that an attacker-modified client-side won’t fool the server. But there can also be pitfalls. While you can use $_FILES[‘userfile’][‘size’] to check the file size, $_FILES[‘userfile’][‘type’] isn’t reliable to know the type, since it’s browser-provided. Use mime_content_type() for that.
Redirect URLs
Sometimes when we send visitors to a page to go through some process, we might want them to return or go to another page after completing that task. For example, we might want someone to be redirected to /dashboard.php after logging in. Since the destination may vary, this target location cannot be put in the code. To solve this, we pass the target URL to login.php as follows:
login.php?redirect=/dashboard.php.
Or we can use cookies for the same purpose.
Finally, the redirection is accomplished by inserting code like what follows in the on-success part of login.php:
if(isset ($_GET[‘redirect’])) header (‘Redirect: ‘ . $_GET[‘redirect’]); else header (‘Redirect: /’);
This will work fine. The problem is that an attacker can create a login page URL like the following and send it to innocent users to visit: https://yoursite.com/login.php?redirect=https://attcakersite.com/success.php.
Visitors will be concerned about the login page only. If it looks legit, they won’t try to validate the subsequent pages.
The solution is to validate the redirect URLs before setting the header, or to accept code names (e.g., dashb) instead of full URLs and map them to legit URLs before including them in the header.
SQL injection
Don’t worry; I’m not going to elaborate on this topic. For those who haven’t heard about it yet, SQL injection is a process by which a user enters cleverly formatted text that gets executed as SQL when embedded in your query statements. To prevent this, whenever you earlier used the following code:
query( ‘... WHERE something=”’. $_POST[‘something’].’”’ );
…now use the code below, instead:
query( ‘... WHERE something=’. mysqli_escape_string($_POST[‘something’]) );
Or even better, use something like PDO prepared statements.
Configuring PHP
Before going further, let us check how to tweak the PHP settings. PHP configurations are stored in the file php.ini and in related ones. Write a simple script that calls phpinfo() and visit it from the browser to know their locations.
The main file may not be accessible in shared hosting environments, but many settings can be set in the beginning part of your scripts using ini_set(). You can see a usage example in the next section.
Session hijacking
We all know what a session is. HTTP is stateless and a session helps us to remain logged in (or identified as the same person) while hopping from one page to another in the same site. Once logged in, the server sets a session-identifying cookie in our browser, and the browser sends this back each time we visit a page in the same site.
This means that if an attacker manages to steal the cookie of a legit logged in user, he can place it in his browser and log in as the same user. Session hijacking can also be done by other means, but let’s start with cookie stealing.
One way to steal a session cookie is to perform XSS. Attackers first create a profile in the site, inject some script to their own profile, and when others visit their profile, the code gets executed in the browser of the visitors. The attacker can use document.cookie() to get the victim’s session cookie, and get it sent to him using Ajax.
There are various measures to prevent session hijacking techniques that include cookie stealing:
- Use HTTPS
- Tell the browser to safeguard the cookie
- Make sure the server-side session data is inaccessible to other sites
- Use session IDs that cannot be guessed
- Regenerate the session ID periodically
The first one is a must. We’ll discuss the third precaution later. PHP session IDs aren’t that easy to guess, so the fourth preventive measure is also checked. Regarding the fifth one, there is a function called session_regenerate_id() to regenerate the session ID, but it can harm the user experience.
What about the second precaution, then? Normally, you shouldn’t trust the browser since the user can modify it or can create a custom one. Plus, there can be malware. However, since cookie stealing is never done by somebody against oneself and we’re trying to protect the user from third-party scripts, we can trust the client-side in this case (unless there is malware).
The following php.ini settings will be a good start:
; Prevent access from JavaScript ; (no longer accessible via document.cookie) session.cookie_httponly=On ; Delete the cookie when the browser is closed. session.cookie_lifetime=0 ; Do not accept the session ID from GET/POST/URL session.use_cookies=On session.use_only_cookies=On ; Do not accept a session ID provided by the user session.use_strict_mode=On ; Enforce HTTPS (SSL/TLS), if the site supports it session.cookie_secure=On
Many settings can also be updated using ini_set(). For example:
ini_set(‘session.cookie_httponly’, ‘1’);
In this case, make sure to place these calls before session_start(), preferably at the very beginning.
To get authentic session security advice, visit https://www.php.net/manual/en/session.security.php.
Session conflict
The values you get from $_SESSION are kept on disk in places like /var/lib/php/sessions/. Needless to say, it should have tight permissions. But even with this place being protected, attacks can be possible. Think of a server in which two different sites share the same PHP installation/environment. Both sites use sessions, and the session keys of both are stored in the cookie PHPSESSID on the client-side.
No problem — since the browser will check the domain name before sending back the cookie. But what if the visitor intentionally sends the cookie to the wrong site? If Site A sets the cookie and the user sends it to Site B, the latter will accidentally read the session data from Site A because they share the storage area. If the user has logged in to Site A with a user name with which there exists a user in Site B, he might actually be permitted to use the site as that user.
Setting special keys in $_SESSION won’t help since they can be scanned. The best option is to use a custom storage path and safeguard it with permissions. To set a custom path, update session.save_path, either in php.ini or via ini_set().
Equality checks
The ability of PHP functions to return values of any type may feel like a blessing. On success the function can return the result and on failure, the function can return FALSE. There’s no need for exceptions or additional error flags. Most of the built-in functions follow this pattern.
Although convenient, this can be a dangerous feature if your error checking is faulty.
== and != will confuse FALSE with 0 and TRUE with any non-zero values. This is not a problem in many other languages since functions cannot return values of multiple types. But in PHP, if your _get_user_id() returns FALSE on failure and an integer otherwise, the call will mistakenly be understood to have failed because it returned a valid user ID 0; and comparing it with FALSE, using == , finds a match.
Read the documentation before using any function and see if it has this pitfall. Use === and !== (which are type-sensitive) instead of == and != for error checking if the function returns something like TRUE, FALSE or NULL. Although we stressed on return values, these operators can be used on regular variables also.
Where to place the scripts
All the user-accessible files are put inside the Document Root (usually called public_html) and its children. This includes PHP scripts, which are executed instead of being displayed. But when something goes wrong (bad configuration or a bug in your server software), scripts may get displayed in the source form. This shouldn’t cause any security issues since your site has to be designed in a way that it is secure even if its full source is revealed to the world. But what about things that should be kept secret? Like the MySQL password (which cannot be hashed)? Or some experimental code?
You can place them somewhere outside the document root, and include them in user-accessible files using include() or require().
Own server versus shared hosting
If Website A is the only one hosted on the server, all you have to worry about is how to protect your site and data from visitors (not that this is easy). But if the site is on a shared server, you have to worry about other sites accessing and interfering with the data and operations of the site in question. Even if all other sites on the server are owned and operated by you, problems can arise. We’ve discussed one case already regarding session conflict and how to mitigate it using session.save_path.
File permissions
Regardless of where they are placed, any of your files can be read (and even modified) by other sites in a shared hosting environment, if you didn’t set the right permissions. While setting permissions, ‘others’ means other users/sites on the server, not your visitors. So feel free to disable even read access for others.
Sometimes, setting owner-only permissions might prevent the server software from accessing your files because it runs as a separate user. In an ideal shared-hosting environment, Apache or similar software should run each site as its own user. You can easily accomplish this by installing appropriate modules if you own the server.