When it comes to securing any backend application, we need to consider three factors:
- Source code;
- Storage of sensitive credentials;
- Deployment pipeline and security of the hosting environment.
Each factor brings unique challenges, and although this article focuses on applications running in the Node.js environment, the same principles can be easily carried over to any other system.
Source code security
Enforcing security-first practices during the early stages of development is important. While it does bring additional overhead in terms of configuration complexity, any time investment is ultimately worth it.
Source code security can be further categorized into:
- Authentication, authorization, and the use of strong encryption/hashing algorithms;
- Introduction of middleware such as Helmet;
- CORS configuration;
- Protection against CSRF, XSS, and SQLi;
- Rate limiting mechanism to prevent the severity of DoS/DDoS attacks.
Authentication, authorization, and encryption/hashing algorithms
First, let’s clarify the difference between authentication and authorization.
Authentication refers to the process of verifying user-provided credentials (username, password, etc.) in order to validate their identity. Authorization refers to determining access rights and permissions for a given user.
Node developers typically choose Passport.js. Authentication strategies differ based on a use case, from basic username/password to federated OAuth/OpenID and more common token or session-based. Recently, there’s also been a surge in popularity of SSO (Single sign-on) schemes.
Always consider the client-server trust over ease of implementation. Oauth2 has become a contemporary standard, especially the OpenID Connect, which is a simple identity layer on top of the OAuth 2.0 protocol.
Regardless of the authentication policy, authorization typically uses a combination of RBAC (role-based access control) or claims-based authorization. RBAC works by checking for specific user roles, while claims-based authorization compares permissions assigned to the user. Node.js developers typically use CASL, which allows for simple scalability between authorization strategies, for example, from claim to subject/attribute-based. If you use a Nest.js framework, you would typically protect the endpoints with guards.
Lastly, no authentication or authorization mechanism will be of any value, unless you implement strong encryption and hashing algorithms. You should consider AES (Advanced Encryption Standard), ECC (Elliptic curve cryptography), or PGP (Pretty Good Privacy). Algorithms such as DES are not considered cryptographically strong.
Helmet is a popular middleware that works by automatically setting various HTTP headers. It shouldn’t be relied upon as the only system you put in place. It acts as a wrapper around 15 different middleware functions. I will not cover each in detail, as it falls outside the scope of the article.
Browsers restrict cross-origin HTTP requests, these are requests which come from a different origin (in plain text, from a different URL). Since APIs are usually consumed by clients that do not share the same origin.
For that reason, CORS needs to be enabled. Enabling CORS to accept requests from any origin is only acceptable when there is no authentication/authorization involved, and there should be no restriction on the access of resources. Instead, you should think about restricting requests to domains you are aware your clients use. For example, unless we are dealing with a publicly-available API, and you know where your frontend is located, limit the acceptable origin to the frontend alone.
If possible, skip cookies. They expose you to potential cookie-related attack vectors.
Protection against CSRF, SQLi, and DoS/DDoS
When a Node.js application operates with sessions (and/or cookies) and performs actions on input from authenticated users, CSRF (cross-site request forgery) attacks are almost certainly inevitable. Using csurf library, we mitigate the possibility of such attacks by generating so-called CSRF tokens. These tokens contain significant entropy (making them strongly unpredictable) and are typically transmitted from the server to a client in a hidden manner. Clients then submit a form with the token, and the server compares the issued value with the received one.
Without csurf, you can mitigate CSRF attacks by only using JSON APIs, disabling CORS (which, as we discussed above, might not be always possible), and making sure HTTP GET has no side effects (this means, it doesn’t change any relevant data in a database).
SQL injection occurs when an attacker can craft input that is falsely interpreted by the application to execute a database query. The easiest mitigation technique is parameterized statements, however, an even stronger alternative is an ORM (object-relational mapping) framework. These typically come with built-in support for field validation.
Thus, using parameterized statements, ORM, and input sanitization is a great starting point.
Rate limiting, as the name suggests, deals with limiting the number of requests a client can make in a specified period of time. This helps divert potential DoS/DDoS and brute force attacks.
Storage of sensitive credentials
Sensitive credentials, like API keys and database connection credentials, need to be stored in a secure manner and kept hidden during distribution between development and staging/production environments.
For that particular reason, one should consider the following:
- Never store secrets inside databases: if a database leak occurs, sensitive secrets might most likely cause a potential attacker to completely overtake the system
- Never hardcode secrets: just like a database leak, a source code leak is a common occurrence in the software development field (perhaps even more common). Code itself is not immutable, hence from a practical standpoint, you’re working against yourself.
Modern approaches to storage and distribution of sensitive credentials include VCS (Version Control System) hooks or tools like git-secrets/git-crypt. Outside of version control systems, we can use configuration management systems such as Ansible Vault, or some external secret management service. For example, AWS has a service called AWS Secrets Manager.
Deployment and hosting environment
Ultimately the backend application will reside outside the developer’s machine. We need to secure the following segments:
- Remote configuration via SSH, use of firewalls, honeypots, and VPNs;
- Virtual private clouds (VPCs);
- Enforcement of TLS;
- Regular backups.
SSH, firewalls, honeypots, VPCs
SSH (Secure Shell) protocol rightfully replaced Telnet, the unencrypted remote configuration tool. SSH provides encrypted connection and bidirectional communication path. It relies on public-key authentication which is inherently safer than passwords. However, SSH key management possesses about the same level of danger as the use of security-inferior remote access technologies.
From remote configuration, we move to firewalls and honeypots. In a typical cloud deployment scenario, the use of firewalls is greatly encouraged. Firewalls enable filtering control over ingress and egress traffic. For example, it is discouraged to expose an SMB service to the outside world, as that can lead to man-in-the-middle attacks, privilege escalation attacks…
Honeypots are not necessarily a solution everyone should reach after, however, consider your options. Honeypots are used to trap potential attackers to “fake” services and divert them from the actual location of your business infrastructure. Nowadays you can use a variety of open-source options, or investigate a setup with your cloud provider.
Virtual private clouds (VPCs) enable the provision of isolated clouds within the public cloud infrastructure. This way, the computational resources are hidden. VPCs, therefore, enable users to define their own network space and finely tune security rules within it.
It goes without saying that even with everything mentioned above standing firm if your API does not enforce TLS, all you need is a successful man-in-the-middle attack.
Unfortunately, the software is prone to errors; and that does not only refer to one we write. Cyberattacks and bugs together can cause you to lose running data.
Regular backups take data snapshots and enable us to retrieve them at any time. Follow the 3-2-1 backup rule:
- Create one primary backup and two copies of your data
- Save backups to two different types of media
- Keep at least one backup file offsite
Luckily, you don’t need to handle backups yourself, and major hosting providers, like DigitalOcean, automatically do them for you. If possible, databases shouldn’t be the only data you address with your backups, thus aim for a holistic approach.
Hopefully, this article gave a high-level perspective on the security of Node.js applications, and backend in general. Of course, a lot more than just an article is required to establish a sound end-to-end security of your systems, which is where penetration testing comes into place.
If possible, companies should eventually need to consider investing time and resources in a system security check. Better safe than sorry.
Written by Peter Aleksander Bizjak