Web security at N26
There is a popular saying that goes, “The web is accessible by default; it’s our job not to screw it up.” In many ways, security feels like the opposite. It’s poor by default, and you really have to work hard to make it decent, even more to make it good.
N26 being a bank, this is a topic with which we’re not messing around. Over the last year, the Security Team and the Web Team have been working closely together to dramatically increase the robustness and resilience of the N26 Web platform. This article is a tour of some things we do to protect our customers from various attacks and risks.
Protecting against DDoS
As a solid banking contender in Europe — and soon to be in the United States — N26 is more and more a potential target for Distributed Denial of Service attacks (DDoS for short). These attacks usually consist of unleashing a vast amount of traffic onto a server in hope of overloading and shutting it down. Maintaining such an attack over long periods of time is a common tactic to extort companies into paying ransoms.
The first line of defense against denial of service attacks is to impose some sort of rate limiting. That’s technical wording for blocking someone (an IP) from performing too many requests under a certain time frame. This ensures someone cannot automate thousands of requests per seconds or minutes against a server.
The reason this is not enough is because proper attempts at DDoS are unlikely to come from a single IP. A more advanced attacker might put their hands on hundreds, if not thousands, of low-end devices (sometimes called “a botnet”). Basically, anything that can access the internet and has default access passwords such as fridges, baby monitors, talkies, and whatnot; welcome to the Internet Of Shit™. With this remotely-controlled army, they can hammer a server with hundreds of thousands of requests coming from hundreds of IPs.
These types of attacks is way more difficult to defeat, especially if every device maintains their query rhythm just below the restricted limit. To help us counter these invasions, we have a proxy acting as a funnel in case of overload: it will slow down, abort or deflect requests so only a certain amount reach our services in a certain time frame. Think of it as a digital bouncer. On top of this and other various defense mechanisms, which we won’t discuss here, we have horizontal scaling in place for all services: as the load goes higher, we deploy more machines and balance the load across all of them, effectively reducing the load on each machine itself.
Rate limiting however, no matter how well implemented, might not be enough to prevent DDoS attacks entirely. Another way to slow down a server to the point where it can crash is by finding a speed bottleneck. Consider a request performing heavy image processing, doing validation on a given value or requesting a lot of interconnected data. It might be a computer-intensive request, with a slow response time. What happens if one keeps performing such a request (not necessarily in a super fast way), so many times that the server gets overwhelmed, some may ask?
To prevent such scenarios, we do some smoke checking on the request shape. Is it awkwardly large? Does it have an abnormal number of values? Are some values absurdly long? If we find something suspicious, we abort the request without treating it. Unfortunately, GraphQL is also sensitive to such attacks. Imagine querying information about a user. One property of a user might be their contacts. Each contact is another user entity. Which has contacts. Which are other user entities, and so on. A single GraphQL query could try to query this dozens or hundreds of levels deep, draining CPU resources with it. One way we prevent that is by measuring the depth of incoming GraphQL requests; if they are more than a few levels deep, we abort them right away.
Protecting against impersonation
Lack of availability is obviously not the only risk we have to face. Many attacks — especially ones targeting banks — intend to steal or impersonate users in order to steal their data. They are usually way more creative and their feasibility and impact are very much dependent on the system they target. At the end of the day, all defense mechanisms boil down to ensuring trust. Is the person performing an action really who they claim to be? It turns out this is a harder question than expected.
Locks on the cookie jar
When using N26 for Web, the user’s session (such as their key to communicate with the API) is stored in cookies. There is technically no concept of sessions per se on our web server, which means we are not vulnerable to session hijacking attacks.
CSRFing the security wave
Cross-Site Resource Forgery has been around for basically as long as the web. The concept is simple: a CSRF attack consists of performing an operation on behalf of someone else by taking advantage of authentication cookies. What’s tricky to understand is that an attacker does not actually need to be able to read or modify cookies in order to use them to their advantage.
In our case, it means if you would authenticate on N26 for Web, then visit a malicious website, they could trick you into submitting a form that actually hits the N26 API, therefore performing actions on your behalf without you even realising. While that would be super difficult to actually exploit, that would still be a problem. We prevent that.
Encryption within the browser
So far, one could say everything is just Security 101. Performing encryption on the client before sending requests was quite a first for me though. In fact, when our security team suggested it, I didn’t fully understand what they meant, and whether it was even realistically achievable in the first place. It turns out it is, although it is certainly a challenge.
In practice, front-end encryption works like this: on start, the server generates two keys, a public one which makes its way to the client in a cookie, and a private one which stays on the server. In the browser, the public key is used to encrypt a certain payload before sending it to the server via a XHR request. On the server, upon receiving the request, the payload is decrypted using the private key. It’s important the private key remains a secret and never gets leaked, as it is the only way to decrypt the data.
From afar, it looks like it does the same job as TLS (what enables HTTPS protocol): it encrypts the data between the client and the server. That’s true for the most part. However while TLS is an absolute wonder, it can fail in unlikely and extreme circumstances. Remember the Heartbleed weakness? More commonly, front-end encryption ensures that only the service that needs to access requests’ payload can access it; any in-between services cannot.
For very sensitive information, such as for the secret certification PIN, we perform a similar operation before hitting the backend API. The client sends a request to our GraphQL layer that is encrypted with the public key, which we decrypt on the server. Then, with a different set of keys and slightly different mechanism, we encrypt the sensitive information and pass it in a header towards the backend API. The latter can decrypt it with the private key on that server.
The idea is to have TLS protection during transfers between parties (client to server, server to server), and still have encrypted data (payloads and sensitive headers) when TLS ends (“TLS termination”).
Headers in the cloud
Over the last few years, many standards have been brought up to improve web security, many of which go through HTTP headers in order to make transactions between clients and servers more reliable.
With the X-Content-Type-Options header, we prevent some browsers from trying to guess the type of a resource. This reduces risk of unsolicited downloads and malicious user uploaded content that could be treated as a different content type (called “MIME sniffing attacks”).
The X-Frame-Options header protects us against “clickjacking attacks”. Such attack consists on loading the target website in an iframe, and then layering invisible elements on top of it to capture clicks, keystrokes and other sorts of information. This header makes it impossible to embed any of our websites within an iframe.
The web has a long history of providing insecure content, even though certificates are now cheap and easy to set up. Fortunately, it is possible to instruct browsers to force content to be served over HTTPS with the Strict-Transport-Security header (HSTS for short). You can read more about how we use HSTS at N26.
Protecting against XSS
For once, browsers can help us mitigating against XSS. All our domains enable their built-in protection mechanisms with the X-XSS-Protection header.
Enforcing rules with CSP
The interesting thing about XSS attacks is that a mechanism has been designed and widely implemented to prevent them once and for all: Content Security Policies (CSP for short). A CSP is basically a contract between the server and the browser; the former instructing the latter what can be safely loaded. It covers all types of resources — from images, CSS and scripts to embedding/embedded documents, fonts and XHR requests. For each type, it defines a list of allowed domains. The browser will decline resource requests from a domain that is not explicitly allowed.
In practice however, it is quite challenging to implement a strict security policy, especially when many actors and third parties are involved in a project lifecycle. Take the monstrosity that is Google Tag Manager for instance. It basically gives marketing teams a backdoor into their website’s internals by allowing resources loading and execution. Unfortunately, GTM and/or similar systems are usually necessary in large companies to collect data and drive business. Authoring a secure policy around such a system is tricky, because it basically relies on the fact that it needs to load a bunch of scripts and images from a bunch of third parties in order to work. We are still tweaking our content security policy on a regular basis and are working towards making it as strict as possible in order to prevent all sorts of XSS injections.
Along the same lines, we recently introduced a feature policy, which is a similar concept but for device features such as vibration, geolocation, notifications, camera, and so on. This header ensures we limit the access to the customers’ device capacities to the bare required minimum.
Protecting against ourselves
There is a lot of things we do to make sure the code running on our servers and our customers’ browsers is safe, yet a lot of risks happen entirely outside of this scope. One way for us to consider them is to embed security into our daily work, and stop considering it as a fire-fighting skill or an afterthought.
In the last few years, we started performing regular threat modeling sessions for all projects in which we determine risks, mitigations and trust boundaries. What happens if a dependency gets compromised? What happens if an employee goes rogue? What happens if a service gets shut down? While unlikely, all scenarios are technically possible.
Sometimes, our investigation translates into technical implementation. For instance, one way we minimise the risk of our npm dependencies serving as an attack vector is by auditing them when building our project. It certainly won’t catch everything, but it definitely can prevent some bizarre attack scenarios. If we find a vulnerability in a dependency, our build gets immediately aborted and we have to resolve it.
Security on the web is not easy. It takes a lot of time, it takes a lot of effort and more than anything else, it requires a company to shift from battling security issues to proactively preventing them. N26 has gone through this transition over the last few years, and has thus become one of the most secure web banking platforms.
We are always looking for skilled people to join our security team. If you’re into puzzles, you can give our security challenge a go. If the prospect of joining us is not sparking joy in you, but you’d like to make a few bucks, we are on HackerOne, ready to receive your security-related bug reports.