In our previous post, we talked about how, as Shrek once said, security (and ogres) are like onions: they have layers.
Today, we’re peeling back another layer and diving into the software side of things. Last time, we drew inspiration from 10th-century architecture to explain layered defence, and since we still don’t have enough ogre puns, we’ll continue with that analogy.
Think of your web application as a castle, with the treasure inside representing your data. Your software is the castle’s main building. There are many ways to defend your gold from raiders, but not all defences are created equal, and piling on walls isn’t the same as being truly secure.
Back to the castle: once we found defensible terrain, we need to build our castle proper (i.e., the main building). Every castle needs high walls, guards, maybe a moat, a drawbridge, etc. But they are not the only layers we need. Just like how having high walls and guards patrolling the ground doesn’t mean the castle proper doesn’t have locks on the doors, layers of defense in our infrastructure don’t mean we have to ignore the security layers in our web application.
All these layers work together towards protecting our treasures, but security isn’t about piling on layers just because. You don’t just add more layers since it’s not the number of layers of security that is important, but rather how effective they are.
Having the right layers can be made easier by choosing the right framework. When choosing a framework, make sure to choose one that puts security first. Django is one such framework. I don’t want this article to sound too much like an advert for Django, but there is a reason it was the inspiration for our company.
It incorporates built-in security features to protect against common web vulnerabilities, which makes it the best choice for developing secure applications for us. Django also encourages secure practices through its architecture and documentation.
For example, Django ORMs protect against SQL injection if configured properly, and Django also comes with other built-in security measures against Cross-site scripting (XSS), Cross-site request forgery (CSRF), and more. But just like a drawbridge, some of these features require some action on your part. If you disable them for development ease, it’s like leaving the drawbridge down; attackers will simply stroll right in.
If you choose to go with a different framework, then please make sure to spend some time looking at the security features it has before you get started with development.
When building a web application, you will very often use tools and packages made by other people to speed up development or add functionality. This provides an opening for what are known as supply chain attacks.
A supply chain attack happens when hackers compromise third-party dependencies that your app relies on. For example, imagine you use a package called Foo, and an attacker manages to sneak a backdoor into it. Without realizing it, your application now inherits that vulnerability, like accidentally granting the castle’s master keys to a stranger disguised as a merchant.
The key takeaway? Only install what you truly need, and keep it updated.
In software security, attack surface minimization is the name of the game. Every new dependency you add is like another little door into your castle, and the more doors you have, the harder it becomes to guard them all.
This doesn't mean you should rewrite everything yourself, but it does mean you should be picky: choose battle-tested, well-maintained packages, and double-check you’re using the right ones. (Remember: it’s @fortawesome, not @fontsawesome.)
If that sounds like a lot of work, I have some good news. Tools like Dependabot can help lessen the load by automatically creating update pull requests and loudly pointing out known vulnerabilities. All you need to do is review the changes, confirm nothing is broken, and merge. Think of it as a loyal steward checking and repairing your castle gates before anything sneaky slips through.
In our last post, we talked about how a CI/CD pipeline can expose secrets, but we should note that this applies to the application itself. Secrets such as API keys should never be hard-coded into the application but instead configured as environment variables. You do not want a situation where they are pushed to your repository accidentally, so this should be done very early in the development life cycle.
A castle can have the thickest walls and the deepest moat, but if the guards let just about anyone in without checking, it won’t stand for long. The same applies to web applications, it doesn’t matter how secure everything else is if poor user management and authentication practices let anyone have access to data.
Let’s start with passwords. If your users are still able to use passwords like “password” or “qwerty”, then you are leaving the drawbridge open. Enforce strong passwords, I’m not suggesting the overly complex ones with a mix of uppercase, lowercase, numbers, and special characters, but something better than “password”. It might annoy some users, but not as much as a data breach would.
You could use third-party authentication if you’d like, like signing with your Google account, that way it’s Google’s headache about how secure the passwords are, but that has its own problems, like what if they are hacked?
Or, radical idea, skip passwords altogether. Passwordless authentication is a method of verifying a user's identity without relying on traditional passwords or other knowledge-based secrets. Instead, it uses alternative methods like biometrics (fingerprint, face recognition), one-time passwords (OTP), or physical security keys.
Multi-factor authentication (MFA) is the digital equivalent of asking visitors for both their ID and a secret handshake. Even if an attacker gets past the first layer, they’ll be stopped by the second.
Just like other aspects of authentication, you can roll out your own, send users an OTP that expires after a certain time, or use something like Google Authenticator. Each choice has its ups and downs, so consider carefully.
Proper session management and session timeouts ensure that, once a user is idle, no one else can hijack that session and use it for nefarious purposes. So, make sure sessions have appropriate timeouts, they should not be too short, such that they annoy users, but not too long that they are a security risk. Session timeouts should be long enough that a user can get a cup of coffee and still be logged in, but not long enough that they can go on vacation and come back to find that they are still logged in.
Try to stick to the Principle of Least Privilege, i.e., give user accounts or processes only those privileges that are vital to perform their intended functions.
Lastly, don’t forget to regularly review and audit user access rights. If someone was given admin privileges to “just fix one thing” six months ago and still has them, or users who have left the company and still have access to the system, that’s a security risk.
If your application is useful, odds are it talks to some other application. This communication is handled through an agreed-upon interface called an API. However, this brings up the question of how your application knows it’s talking to the right person and that person has the right to access the information they requested or perform the actions they want to perform.
APIs are essential in modern applications, but poorly implemented permission systems can leave them vulnerable to exploitation. Permission escalation allows attackers to gain unauthorized access to sensitive data or functionality by bypassing or abusing insufficient security controls.
So the advice here is to take the time to ensure that your API is secure. Ensure you have appropriate authentication and authorization in place and that all requests are made and fulfilled over encrypted connections.
You might also want to look into rate-limiting your API, especially if it’s publicly accessible.
In the database section of our last post, we spoke about protecting data in a database, but this is not just a database issue. Since your web application will talk to the database, you need to make sure that hackers don’t manipulate that data through your application.
One common type of attack I’d like to focus on is SQL injection. SQL injection exploits vulnerabilities in web applications to inject malicious SQL code into database queries. I couldn’t come up with a great castle metaphor, so here’s one from Star Wars. SQL injection is like that scene from A New Hope where Obi-Wan Kenobi tricks a Stormtrooper by saying, “These aren’t the droids you are looking for”.
In an insecure web application, if you just take whatever the user puts into an input box and send it to the database, you run the risk of hackers writing something like “‘); DROP TABLE users;” This closes your first SQL query and runs a delete command. Suddenly, you have a lot of angry users who have lost everything.
The takeaway? Your user inputs should be sanitized to prevent this. Again, your choice of framework can make this a trivial problem to fix.
I said it before, but it is worth repeating: It’s a fact of life that there will never be enough time or budget to do everything you want to do. But your web application still needs to be secured, so you need to make the most of the time and budget that you have.
Start by strengthening what already exists. If your framework has opt-in features, then opt in and configure them.
You can add additional layers of security, but in my experience, that’s not always the best approach. Additional security layers also add additional complexity to your system and can add additional vulnerabilities.
Adding additional layers comes at a price, either in complexity or cost; adding an external firewall from a big-name provider can cost an arm and a leg, while rolling out a free one will cost you your time.
XSS is a big problem in web applications, and Content Security Policy (CSP) is its solution. CSP restricts what scripts can run on your site, meaning you determine which can be trusted and ensure hackers can not easily inject malicious code.
CSP has given many a developer headaches, but it’s worth doing right. Think of it as a “no unauthorized swords” policy in the castle. You want your guards to be armed, but not random people. A CSP policy that allows anyone, or no CSP policy at all, is like letting any Thomas, Richard, or Henry walk into the castle with a cart full of swords.
There are more measures that can help deal with XSS, such as filtering input on arrival, encoding data on output, and using appropriate response headers. Again, your choice of framework, like Django, can make all this much easier.
Once your system is up and running, don’t assume it’s secure, attack it, err, I mean test it. Hire security experts or use automated tools, do anything and everything to find weak spots before real attackers do.
Check for common vulnerabilities like SQL injection, brute force attacks, and session hijacking. Try logging in with weak credentials or try to exploit forgotten admin accounts to see if any sensitive data is exposed. If you can break in, so can someone with bad intentions.
The key takeaway is simple: web applications, like Ogres, have layers. And each of those layers can become a backdoor if not properly defended.
Vulnerabilities rarely live in isolation; they often span multiple layers. Your goal is to make sure that if one layer fails, another catches the threat. If your frontend misses an attack, your backend should stop it.
This turns your web application from having multiple layers of vulnerabilities to having multiple layers of defence.