What can we do?
As demonstrated above, there are many creative and unexpected ways in which our software can be compromised.
This might seem depressing. A single mistake or negligence can leave us exposed. But all is not lost. There are plenty of things we can do to keep our systems safer. Many systems are vulnerable because of not applying ideas from past lessons learned in the software industry.
Here is a list of security measures you can employ for your systems and your code to protect them against known security vulnerabilities. As you read, you will find that many of them are directly related to the exploits we discussed above.
1. Keep your systems up to date.
After a vulnerability is documented, operating systems, frameworks and libraries eventually get fixed. It is mostly the systems that are left behind, the ones that are still not updated weeks/months after the vulnerability is documented and a patch is released who fall prey. These are easy targets for anyone that are aware of the vulnerability. And probably at that point, even a proof-of-concept exploit code is floating around for anyone to read, adapt and use against you. Keeping your systems up to date is a fairly cheap method to protect yourself, and it’s worth the effort.
2. Reduce the attack surface.
Many easy targets in your system can be eliminated by reducing the exposure to just what the application requires at a minimum. Reduce the attack surface by disabling any features that you don’t use, closing endpoints that don’t need to be exposed, removing plugins/extensions that are not necessary, and reducing permissions according to the principle of least privilege.
3. Keep your production environment/network secure.
Leaving parts of your network unnecessarily open to the outside world, or using network devices with outdated/vulnerable software can put your systems at risk. Networks used in the production environments should be restricted to limit exposure to the outside world so only necessary endpoints are accessible. They can also be divided into multiple isolated networks, according to the application/containers that are running in the environment, their possible endpoints, and interactions. A monitoring solution should be deployed to keep track of the network and generate alarms if something goes wrong. The whole system should be designed to be clean, simple and automated as possible so that it is easy to take action in case of an emergency. Penetration testing can be employed.
4. Monitor your system.
Deploy monitoring solutions, and configure them to generate effective and actionable alarms for any suspicious activity. Events to be monitored may include database query rate spikes, data flow limit overflows, server malfunction or shutdown, application error rate spikes, and suspicious network activities.
5. Learn the configuration in detail.
The tool/server/database you are using might not have a secure default configuration. This is a lesson often learned the hard way. Maybe it has a distinct development configuration that lets you evaluate it for a while locally, or maybe it does not, and still comes configured as a kind of a development server which listens for outside connections to your public IP and do crazy things when asked. We cannot know until we read and understand the configuration, and explicitly define our choices in the configuration file.
6. Automate if possible.
Humans make mistakes, so automate. Automate your build, test, deployment and configuration process. Automate everything you feasibly can. All hail the robots.
7. Keep an eye on your service providers.
Third party services we use such as CMS, storage, SAAS and cloud providers, network service providers and other software services can also be openings for possible attacks, since a successful attack on them can render your security measures about your systems useless. These services should only be utilized after evaluating them from a security standpoint. Also monitoring solutions can be deployed to detect suspicious changes in provided services.
8. Hand-pick and validate your dependencies.
A library or component you use has the potential to expose your entire application. Therefore knowing which code you use, how and where it is released, the entity/organization that releases it, its release cycles and its visibility in terms of vulnerabilities are important. Keeping the list of dependencies short and sources reputable makes it more unlikely to have security problems related to external dependencies.
9. Take advantage of best practices.
Repeating well-known mistakes with an extremely confident attitude is a popular pastime among developers. On the other hand, there are much better alternatives to spend time on. Such as;
- Employing fine-grained access control.
- Employing hashing and salting for passwords.
- Knowing the basics of encryption, and how symmetric/asymmetric ciphers work on a basic level.
- Knowing what TLS/SSL is, why is it important, and using it for all communications with/between our services.
- Knowing about networking basics and firewalls, and being prudent about your network.
- Knowing about well-known openings/attack vectors in your specific field (e.g. XSRF for frontend developers).
- Disabling that default error page.
- Disabling that default error page.
Note: Yes, I wrote “Disabling that default error page.” twice.
10. Keep your code clean, and maintain effort for good architecture.
In clean, well-maintained code that has a good architecture, mistakes are much easier to notice and fix. If your code is full of workarounds and hacks, and it resembles this (It’s an 8-bit computer made using breadboards and jumpers and logic chips. Isn’t that cool), you will have a hard time even to find the source of a bug/vulnerability you are already aware of, let alone fix it in short notice, or to detect an unknown one in the code during review. Additionally, good architecture is one that is designed with security aspects in mind, such as access control and validation; so there is a good chance a vulnerability won’t be able to find its way into your code at all during development.
11. Write code with security in mind.
Writing code to be secure from the beginning is the best way to approach application security. On the other hand, defining all-encompassing rules for writing secure code is hard; but we should have some nonetheless. Below, there are some rules compiled by analyzing popular vulnerability types, so they can be mitigated from the beginning when writing new applications.
Although there can be exceptions to following rules, they have been proven themselves to be foundational. So as always, take them with a grain of salt; but consider the fact that not following them caused many exploits in the past.
Always validate, clean and filter.
Outside input should always be validated against rules that explicitly define what the input should exactly contain. The length of the input, which characters/binary symbols it could contain, the pattern of the data, and restrictions on which values a field can have are some of the things that can be validated. Also, the structure of the input and its fields should be validated if it is in document form (e.g. JSON, XML, YAML). Input data should be validated as early as possible, before executing any logic, that would assume it is already valid. Many libraries can be found for this task for many different languages, such as commons-validator for Java and voluptuous for Python. If you are developing a web API, you can also consider using an API standard such as OpenAPI, which also has support on many libraries and platforms that are used in API development.
Don’t use evaluators. It may be tempting to just get the query as an input parameter and pass it to the database, instead of parsing and validating each query parameter and passing them separately to a carefully implemented repository abstraction. Or it might be tempting to pick what method will be called on runtime by evaluating the input with a scripting engine. Don’t do these things. Write explicit, unsophisticated and understandable code that can serve no other purpose than what it is designed for.
Don’t use scanners. If you think your code should search for what method to call using a code scanner (e.g. reflection for Java) at runtime based on data contained in an input string, I would tempt you to reconsider. Wouldn’t a simple switch statement do? If you do not actually need them, scanners can be off the list. Even if you do need them, there might be other options.
Pay attention to error cases. Think about how errors/exceptions may be thrown and in which combination in your code, and what the resulting flows are. Try to keep exception handling as simple, consistent and regular as possible through your code. Poking around for error messages is a method used by attackers to gain information about the inner workings of a program, and errors themselves can also be used for exploits.
Mind what you are returning in a response. Make sure a response doesn’t give any information about the internal state or operation of the software to the outside world. This is also true for any error messages. Restrict the returned data to only what is necessary for the client to resume the process.
Add detailed, meaningful logging. If something goes the wrong way, it will only be you and your logs. Design your logs so that you can trace a story/transaction/process/whatnot from beginning to end. Add too much detail, and your log database will drown in it. Add too less, and you won’t even know what happened, or if it happened. Not fun.
12. Have a rescue plan.
How long would it take to restore it all back from code repositories & cold backups from scratch, if all of your systems were compromised? What if only some of it is breached? What would be your detailed plan of action, from beginning to end? Laying out these plans early can be very helpful if an event occurs, and at best, help you to discover the weaknesses of your system that you didn’t realize before.
13. Keep an eye on the news.
Keeping an eye on blogs and articles about security, and vulnerability databases such as CVE can help you become aware if your system/software has an already known vulnerability so that you can patch/update your systems before they may be detected and exploited. Vulnerability monitoring solutions are readily available for automating this task.
14. Solve problems as you see them.
Postponing a fix for a security issue is extremely risky, and its costs can far outweigh any benefit it can provide. Yet it is a mistake often made. So once a security vulnerability is detected, it should be fixed immediately, once and for all, before adding new features. Postponing the fix, and hoping no one will find out about the vulnerability is a very important and common mistake that is made about software security, with bad consequences.
Software security vulnerabilities are real threats, and keeping a system secure is a hard task. But on the bright side, it is possible to secure a system in a way that we force the attacker to find an entirely new and unknown way of attacking it. We can do that by not repeating mistakes that are made before.
This is possible through following security best practices on all layers of our system, and importantly, allocating necessary resources for maintaining software security in our project, and doing the right prioritization.
After recognizing these threats, one might think that as the next logical step, not to repeat past mistakes, we should not change existing infrastructure, systems or frameworks, or we should not develop new ones; just to avoid security risks.
I think, on the contrary, the new tools can be more secure, simple and elegant than before, and they should be created. The important thing is while we create them, we should not forget the lessons learned from the past, and make our decisions in an informed, rational way.