You probably hear it a lot: you should make your code secure! But… how? When it comes to security, there are a plethora of measures you can implement. Where do you start, and how do you know you’re doing the right thing?
Many blogs trying to help you in this area use a lot of jargon, making it hard for others to read and act on it. I’m here to help you with this, starting with a small explanation of what we’re trying to achieve with cyber security. This blog post is written for Software Developers specifically, no matter which language you program in or whether you’re a front-ender, back-ender, or full-stack.
The goal of cyber security
Initially, you’d say the goal of cyber security is to keep malicious actors (both human and non-human) out of your systems and non-public data. However, these days it’s not just a matter of getting hacked or not. It’s more a matter of getting hacked as little as possible with the lowest impact possible.
For most companies, cyber security is not their primary business. For most malicious actors, cyber security is their primary business. This makes it very difficult, especially for small companies, to fully keep malicious actors out of their systems. Even more so, given the limited resources a company has versus the most often less limited time malicious actors have.
There are several types of malicious actors, each being more persistent than the other. While one type, called Script Kiddies, only executes a simple publicly accessible script to scan machines for known vulnerabilities, another type might have many millions of dollars (and thus, much time and resources) available at their disposal to discover unknown vulnerabilities in your systems. How far you need to take your measures depends on the type of your business.
Your goal should be to make it too time-consuming for malicious actors to try and compromise you and simply continue to their next target. Additionally, assume they can penetrate your systems and design your systems for this by also implementing measures behind your "front door". A common security practice is applying multiple layers of defenses, instead of just one. Think of it like a medieval castle, which doesn’t just have a moat around it but also several layers of castle walls. The idea behind it is if one layer fails, it doesn’t mean a full compromise of your system.
There are a few measures everyone can (and should) take, no matter what business you’re in. I’ll discuss five measures you can take right now.
Validation is a general measure that also improves system stability. You can think of it as your first line of defense as it limits the freedom a malicious actor has to find weaknesses. If all input is validated for the types you want, you’ll end up with additional benefits: cleaner (and therefore more usable) data, a more stable system, and your users will end up with fewer error messages. This means if you expect a postal code you validate it’s in the proper format. For example for The Netherlands, it’s like '1000AA'. With this validation, there’s no wiggle room for a malicious actor to try anything malicious (as far as I currently know). It should be mandatory to have validation performed at least in the back-end for the security because the front end can always be circumvented by malicious actors. For user experience, you should also implement the same validation in the front-end.
However, be very careful of being too strict with validation as that will harm your user experience. For example, if you add validation to a postal code with the format '1000 AA' (including the space!), it will be annoying for your users if they don’t add the space ('1000AA'). You should either automatically add or remove the space in your system to help your users and keep the data clean.
2. Keeping software up to date
One of the easiest ways to hack a system is by simply executing a generally available script that is targeted against a specific component version. It’s also a method that is commonly used by malicious actors. We can defend ourselves against this by making sure we keep our software (e.g. used libraries, components, and also commercial products like your database, web server, etc.) up to date, both on our front-end and back-end. For our custom-written software, this can be tricky if you don’t have the proper CI/CD (Continuous Integration / Continuous Deployment) and test environments set up to automatically test for regression issues.
There are tools out there like Dependabot and Renovate which automatically update the used libraries using a Pull Request which should also trigger your CI/CD pipeline. The Pull Request can even include the full release notes of versions since the version you were using. If you have enough trust in your pipelines, you can even automatically merge the Pull Requests when your pipeline succeeds. This makes it fully automatic. Another benefit of keeping your software up to date is you’ll have less pain upgrading in general, and the pain will come more spread out over time.
One thing to note when completing your Pull Requests automatically is you might miss out on migration notes, which can impact your software silently.
3. Limiting access rights
Limiting access rights is all about impact management. Once a malicious actor has found a loophole in your defenses, it’s best if they can’t leverage other useful functionalities. To prevent this flaw it is important to always use accounts with limited access rights. How limited is again dependent on how tight your security should be.
I’ll give two examples below to illustrate how we can solve this. These are just two situations involving access control, but it’s much bigger than that. You can also think of access rights on a functional level, or even on a technical function/API level.
Example one: arbitrary command execution
A malicious actor has found a way to execute arbitrary system commands on your server. When executing these commands, they’re executed with the same privileges as the software is. If your software is running under root (*nix) or Administrator (Windows), it is a jackpot for hackers as they’ll now have full access to everything on your system, and will be able to use it as a stepping stone further into the network.
Solution It is common to run your application under a dedicated user and explicitly only give the permissions you need. You can think of file permissions, but also network permissions and for *nix even the shell ('/sbin/nologin' to disable the shell for example).
Example two: SQL injection
A malicious actor has found a SQL Injection vulnerability in your system, allowing for custom queries (both read AND write!) in the database. The acquired privileges are again the same as the database user used for the vulnerable query. Sometimes this can even include executing arbitrary commands on the database server.
Solution You can for example use a different user for schema creation and your main application, or even use a different user specifically for the login procedure.
4. Proper access control
Not all data or functionality should be accessible to anybody. This is where access control comes into play, which is a combination of authentication (establishing the identity) and authorization (what can the identity do and see).
One of the things which often go wrong when implementing proper access control is failing to properly check whether the identity has the proper authorization to read/write a piece of data or execute a function. For example, user A may view their invoice by browsing to /invoices/1, where 1 is the invoice ID. An invoice with ID 2 may be for user B, but user A tries to access it by simply browsing to /invoices/2. If there’s no proper access control implemented, user A will now be able to see an invoice belonging to user B. This is a very common example, unfortunately.
In this scenario, there are several solutions that we can combine to make our security even stronger. First, we must verify for each URL (and thus, the accessed data) if the current user is authorized to access it. Second, we can decouple the exposed IDs from the internal database IDs and make them non-guessable.
5. Limiting internal system information
One of the first things a hacker does when targeting your application (either manually or via tools) is gathering information about the used components and their versions. By default, web servers for example expose this information via the HTTP Server header in the response. This is very helpful for hackers because they’re just one Google away from listing all known vulnerabilities for the component. This can be, and most commonly is, even automated.
The HTTP Server header can often be edited by a specific configuration. The least you should do is remove the version.
Another step hackers often take while gathering information is filling erroneous data in forms and fields to hopefully get an error page with a stack trace. Stack traces can contain the names of used components or even detailed information about the structure of your database for example. This is a treasure full of information for hackers, which they can use in further attacks. You can solve this by configuring dedicated error pages or by disabling the display of stack traces (sometimes also named Enable Production mode).
Now you’ve got a small grasp of the first measures you need to focus on, you might ask yourself "What’s next?". The OWASP (Open Web Application Security Project) is a primary source for almost everything cybersecurity-related. Have a look at the OWASP Top 10, detailing the top 10 most common weaknesses and how to solve them. You’ll also find here more information on the measures named in this blog post.
1. Want to implement Dependabot in Gitlab? We have a tutorial for you on our blog. 2. More information can be found on OWASP: Insecure Direct Object Reference Prevention Cheat Sheet