Security tips and best practices for web developers

Posted in Security, PHP, 1 year ago Reading time: 11 minutes
image

The inspiration for this blog post came from a Mastodon post, asking for some tips about security. Well, I had some, but I quickly noticed that what I wanted to share did not really fit in the 500 character limit of mastodon. So I decided to expand on this subject in a blog post. This one.

Some of the tips here are targeted to PHP and Laravel, because that is what I am most familiar with. But most of them apply to other languages and frameworks as well.

To start with, if you want to be able to build secure apps, you will need to...

Security is about the art of keeping data safe, and keeping attackers out. If you have to create a lock that can't be picked, you need to know how to pick a lock. It is the same for computer security: you need to know how hacking works to build apps that are hard to hack.

But that is easier said than done. Security is a complex and HUGE subject. And it can take a lifetime (or much more) to grasp all required knowledge, especially if you are also trying to learn programming languages, frameworks, keep up with deadlines for work and clients, and have a life outside work. So you might want to limit yourself to the most common ways attackers can break through security.

The "Open Web Application Security Project" or short OWASP maintains a top 10 of application security risks that is updated regularly. Every entry comes with background information, examples and cheat sheets for many applications, making it an invaluable source of information for every developer.

Even with the provided examples, some of the information may be a bit abstract or hard to grasp, so let me give you some more down to earth tips on security.

Ok this sounds like an open door. Of course you keep your OS on your workstations and servers secure with all security updates. But there is more than your OS and your language compilers / interpreters. If you start with a clean Laravel install, it will already install a large set of composer libraries. That amount will grow when you're building your apps. And then we haven't even considered the gazillion of npm packages you might need for the frontend.

We assume those packages are secure, and of course we cannot do a security audit ourselves for all of them. But other people will. Sometimes, security risks are found in libraries, and in a perfect world, they are fixed in new versions. But it is your job to install those newer more secure versions in your applications. So, on a regular basis, run composer upgrade (and npm update) to make sure you have the latest versions. You also might want to monitor sites like https://www.cvedetails.com/ to stay up to date in a more proactive way.

There are tools to automate security updates for composer, for example PHP Security Advisories Database which can check all dependencies of a project for known security exploits. But as it says in the README: "This database must not serve as the primary source of information for security issues" so use it as an additional tool.

Ok, so we have our software up to date, let's start with actual development.

Almost every application will process some kind of input from users. Be it command line arguments, URL parameters, input form forms, uploaded files, etc.

Many entries on the OWASP top 10 are related to malicious user input breaking stuff. It all boils down to one thing: user input CANNOT be trusted, ever.

So that means that all user input needs to be validated, but even when it is validated, you also will need appropriate escaping and sanitizing.

Validation of input means: checking and enforcing that all input has the format and structure you expect it to be. You want to validate all user input as soon as possible. That means: at the start of your application lifecycle. It should be one of the first things you do in your controller. And if something does not validate: stop.

Some examples of validation rules:

  • Check if all required input is available
  • Check if the input has the expected format(s). Do you expect a string, integer, float, boolean, date, array?
  • Check the size / dimensions of the input. For example: do not allow integers smaller dan 0, or strings larger than 255 chars. Make sure always to check both minumum and maximum boundaries.
  • Check the contents of input, if possible. For some input only a limited list of values may be allowed. Or a string may only contain alphanumeric characters, or it must match a regular expression.
  • Check types of uploaded files. Use a whitelist of mime types, but don't rely on the file extension!
  • Check access and authorization rights. For example, if you have a database id as input, check whether that id is valid, and whether the user has the rights to access the corresponding database row.

Laravel has extensive validation features to help you with this, make sure to read through the available validation rules.

Even when you have done everything you can to validate the input as thorough as possible, you cannot always assume the contents are safe. Some things are very hard (or sometimes impossible!) to validate. Data can contain all kinds of possible hidden malicious content that is hard to detect. So you need precautions: escaping and sanitizing.

How and when to escape data depends on where / how the data is used. Here are a few examples, but this list is not complete.

If you want to include a string as part of an URL you want to make sure it does not contain special characters like & or # which have special meaning. One way to do that is using URL encoding which will substitute all special characters with an encoded version like %26 and %23.

If you want to display raw HTML in a browser you will need to escape all special html characters like < and >. You can use the htmlspecialchars function for this. But please note, this is not enough to stop all possible XSS attacks

If you want to display rendered HTML (fragments) in a browser, (for example if you are building a CMS) things get even trickier. HTML is hard to sanitize and it can contain all kinds of dangerous stuff like scripting, risking XSS attacks. It is impossible to prevent this using only escaping/encoding. There is a strip_tags function available in PHP, but also that is not enough to stop all XSS exploits. The best approach would be to use an external module like HTML Purifier to clean up and sanitize the code.

This one is best explained by this relevant xkcd:

For the love of God, please always use SQL placeholders or if that's not possible, at least escape all data. There is no excuse, SQL injection should be a thing of the past.

If you use Laravel you wil probably use the Query Builder and the Eloquent ORM which will do a lot of escaping for you. But even then, take care, especially if you are working with raw expressions.

So we learned not to trust user input, but can we trust the user? If the user has been properly authenticated (for example using a username and password), we may safely assume that the user is the person he / she is claiming to be. But that is not always enough. Besides authentication, we need authorization.

An authenticated user should have access to their own data like email address and name, as well as all related data like their posts, orders, etc. But they should not be able to have the same access to data of other users.

That is why you need fine grained access control. If a user tries to update a post, don't just do a check for the "update_posts" permission. But also do a check if the user is allowed to update this particular post.

A very useful and powerful feature provided by Laravel is policies. Policies allow easy definition of all access control rules. They give answers to questions like:

  • Can the current user create posts?
  • Can the current user view a list of posts?
  • Can the current user update this specific post?

Of course there are many ways to implement access control rules but I found that policies provide a nice way to keep things maintainable and organized.

If you haven't realized it by now: security is hard. It is very easy to do it wrong, opening the way for attacks and exploits. So don't try to make up your own security solutions all by yourself. You will be doomed to make many mistakes others have already made.

So, do not write your own escaping function. Do not build your own password reset functionality. Do not make up your own password hashing algorithm. Do not invent your own "encryption". Unless you REALLY know what you're doing, but even then, why reinvent the wheel?

Instead, use trusted and established libraries and functions, that are battle tested and known to be safe, working and secure. For PHP it is a good idea to use only libraries from Packagist with a lot of users, that are maintained on a regular basis. Check the commit history and the issue list in github. If the last commit was years ago, and there are a lot of unresolved issues, maybe it's wise to go for another package.

In addition to the previous point. Laravel is a very mature and well tested framework. It is intended to be complete, giving you all the tools you need to build stable and secure applications. It has all grounds covered: validation, escaping, authentication, password hashing, encryption, and much more. Learn all the ins and outs of the framework by reading all the documentation. And try to use all those provided features, they've been built to make hard things like security easier.

This may be also an open door to some. I sure hope you are using a good password manager by now. You should not reuse any passwords but have different strong passwords, for every service, and it is good practice to have different passwords even for environments (develop, testing, staging, production) of the same service. That is especially important if multiple environments share the same server.

Source control like git is meant for code. Security by obscurity is a bad idea. The security of your application should not be dependent on the secrecy of the source code. Even when someone has your code, the application should still be secure.

But that means that you need to keep source code and credentials strictly separated. Sensitive data, like your production .env file should never be committed to git. The same goes for other data like certificates and key files. Always make sure to add those to your .gitignore.

If you commit them to git by accident, you should consider those credentials as compromised. Change the passwords, revoke and recreate any certificates and keyfiles.

For most applications, you need a set of data for the applications to be useable. For example, a user to be able to log in, some example data and some configuration. It is sometimes tempting to "just grab a copy from production".

This creates all kinds of potential problems. Every developer needs access to production, which is not a good idea. Also, the risk of data leaks is much higher.

Laravel offers powerful database seeding functionality. With this it is quite easy to fill the database with dummy data that is useable while developing and testing the application. There's no real data, everything is "faked" so nobody cares if the data gets leaked. And every developer can quickly set up a complete database without juggling with sensitive production data.

Another useful feature is signed URLs. It is a mechanism that prevents URL tampering, by adding a signature to the URL. When you change anything in the URL like for example an user_id, the signature and thus the URL becomes invalid. You can even attach an expiry date to the signature, so that the URL will be only valid until a specific time.

This post has gotten longer than I expected. As I said, security is hard, it is a very broad and often complicated subject. With this wall of text we barely scratched the surface. Entire books have been written on the subject. I hope this post was useful though. If you have questions or comments, please let me know!

Related posts

image
Smart generics in PHP

Type hinting in PHP8 has become powerful but it still has limitations. In this article I discuss some ways to use Generics to overcome some of these limitations.

Read more →

image
Thirty years of Debian!

Today, August 16 2023, marks the 30th anniversary of the Debian GNU/Linux distribution. It was the first linux version I installed after ditching Windows completely. And today, Debian is still very relevant.

Read more →

image
Using Sublime Text as a full featured PHP IDE

Sublime Text might look more like a text editor than an IDE, but looks can be deceiving. Read why I prefer Sublime Text above other editors and IDEs, and how you can get the most out of it by using the right plugins.

Read more →

image
How to scan QR codes without camera or phone

QR codes are everywhere. Easy to scan with a phone. But what if you want to decode a QR on your laptop instead of your phone, without a camera?

Read more →