Web security is a huge topic, and this article only intends to cover some of the most basic issues and increase awareness of how carelessness or ignorance can lead to exploits. Ultimately, what you don’t know can hurt you, so it’s in your best interest to learn as much as you can about your site and the technologies it relies on. Here’s a brief run-down of some fairly common mistakes I’ve come across and what you can do to either avoid them or lessen your vulnerability.
Make Rolling Backups
If you do nothing else, make sure you are backing up your site and its databases. So long as you know if/when your site fails (or is hacked) and have the ability to roll back to a known good state, you have little to fear.
Make SURE you practice restoring a site from your backups! An untested backup is worthless. Nothing is worse that THINKING you have a viable backup only to discover that it is actually corrupted.
As your backup needs mature, consider storing them offsite, e.g. on Amazon’s S3 service.
I’m supplying two of my most-used backup scripts for those in need. These are fairly simple, but they will work on shared hosts. They will backup a web site’s files and its databases.
Be Diligent about Passwords
You’ve probably heard this a hundred times before, but it is really important:
- NEVER use the same password twice! Ever!
- Avoid short passwords! Length before strength. Try mixing up combinations of smaller units, e.g. “AlphaBetaCharlie” instead of “abc”. Be creative. You can have strong passwords that are easy to remember!
- Store your passwords in a safe place, e.g. in a password manager like KeePass or in an encrypted disc image
- Change your Passwords Frequently! Literally, put this in your calendar so you remember to do it on a periodic basis. This helps avoid brute-force attacks.
- As a secondary line of defense, you can put an .htaccess password on your manager pages. All this does is slow down a brute-force attack by forcing the attacker to crack an additional password.
Password Protecting Directories using .htaccess
This adds an additional layer of authentication to a site or to a page; it’s not meant to substitute for more robust firewall rules or active filtering, but it is easy to set up.
First, create a username and password in a file that .htaccess can read. This can be done on the bash command line using the htpasswd command:
htpasswd -c /path/to/htpassword/file name_of_user
Next, add the following to your .htaccess file. Be sure you reference the file you just created above:
AuthName "Protected Area"
For some more detailed information on .htaccess passwords, see this page.
Make sure you’ve got an updated version of your operating system, your scripting language (PHP), your database (MySQL), and your software (WordPress, MODx, etc.). Sometimes applying patches can be scary, but it’s a lot less so if you’ve got those rolling backups in place!
I want to make mention of a very useful tool: SimpleScripts. It’s available on Bluehost accounts; it provides one-click installs of many web software packages (like WordPress and MODx) and it will alert you to update them when you log into your cPanel. It’s a real time-saver!
Clean your Room!
- Do not install superfluous/untrusted software on your site! Don’t go dumpster diving for code that’s going to end up on a public-facing web site!
- Shut down services you’re not using (e.g. blog posts) because it takes more time to secure them.
- Do not store backups or sensitive data inside your document root!
- Encrypt sensitive data
- Check permissions.
Make sure you organize your site’s files in a way that ensures that only you have access to sensitive data like backups or database dumps. These should NEVER be stored in the publicly viewable document root!
Cross Site Scripting
Behold the terror that is:
<?php print $_GET['x']; ?>
That code should NEVER be used on a public site because it effectively gives free access to the public to put whatever they want on your site! This type of code often sneaks into pagination links or into code that re-populates forms, e.g.:
<input type="text" name="myfield" value="<?php print $_POST['myfield']; ?>" />
where the ‘myfield’ variable contains something like:
" /> <script src="http://evilsite.com/js/screwed.js"></script>
For a list of values you might want to try pasting into form fields to see if they are secure, check out this great cheat sheet at http://ha.ckers.org/xss.html
In the end, be REALLY aware of any user-submitted data. Users can put their own data into ANY form field, and into any cookie, so anything in the $_GET, $_POST, or $_COOKIE arrays (and also the $_REQUEST array) is inherently insecure and should be carefully filtered. These are like the STDs of the web!!!
For articles about web hacks and some good real-world examples, check out other articles on http://ha.ckers.org/
Don’t Take Cookies From Strangers!
If you thought cookies were somehow immune to the area of security threats, they are not! It’s easy to write your own! The Firefox Web Developer plugin lets you easily create your own cookies. This is great as a web developer, but it can also be a sneaky tool for a hacker to introduce unintended code to your application, so filter cookie contents as carefully as you would the user-submitted forms.
Cookies also store the PHP session id; all $_SESSION data is stored on the server, but the unique key that associates that data with the user is stored in the user’s cookie. If one user authenticates, it’s possible for another user to make requests using that $_SESSION id! Especially with applications that require a login, it’s good practice to get a new session id using session_regenerate_id().
Every time you submit form data, you should write your regular expressions very carefully so your application accepts ONLY clean, valid data. In general, if you can keep input as simplistic as possible, it tends to be easier to secure. Integer only inputs are “safer” than alphabetic inputs. Alphabetic input is safer than input that accepts HTML tags, and so on.
Consider the following filters:
Type-Casting to force integer only values:
$x = (int) $_GET['x'];
Alphabetical Characters Only:
// Accepts only letters a-z (case insensitive)
if ( preg_match('/^([a-z])+$/i', $str) )
A common trick used in phishing scams or to perpetrate click fraud involves iframe-ing a site. Basically, the “trick” relies on the HTML iFrame tag to make one site display the contents of another without being obvious to the casual user.
This is a very broad topic, and there are numerous ways that SQL-injection might be used to compromise a site, but they all rely on the same principle: you construct strings of SQL statements and issue them against your database. If a malicious user is able to put his own data into one of those strings, it’s possible that a user can execute queries on your database that you never expected. This often gets back to form-validation and the ever important task of filtering user-submitted data!
Here’s a not-so-hypothetical PHP example:
$username = $_POST['username'];
$sql = "SELECT * FROM `users` WHERE `username`='$username'";
where $username contains something like:
'; INSERT INTO users (username, password) VALUES ('hacker_dude', MD5('xxxxxx') )
When that executes, 2 different queries are sent to the database instead of one, and if your’e not careful, it can allow an attacker to gain access to parts of your site and delete or steal data.
One strong line of defense against this type of attack is using a robust database driver that allows for the use of prepared statements (available since MySQL 4.1): where you prepare a statement once, then execute it multiple times with only certain defined variables changing. What this does is it allows you to define your query and only let the user supply the variables to be used in that query. This is a much more sensible option than letting the user essentially construct the query from scratch.
More mature database driver libraries (such as phpmysli) will allow you to use prepared statements.
You should also get familiar with escaping quotes in your database queries; this isn’t anywhere as effective as using prepared statements, but sometimes you have to use statements that aren’t prepared, so get familiar with how to escape strings before sending them to the database. In PHP, use the mysql_real_escape() function.
Finally, consider setting up special database users and roles to handle different types of queries. If a query is hijacked, it can only execute with the same permissions as the database handle. In other words, you wouldn’t grant delete or insert permissions to a database handle that was used only for selecting data. It’s more work to set up your database handles this way, but it may help prevent an attack from succeeding.
Watch your cornhole. There’s a lot going on a web site, and there are a lot of ways to abuse the technologies that run them. If you understand how the exploits occur, you’ll be better prepared to prevent them.