Ensuring security during development is crucial, especially as online and e-commerce services become more complex. To mitigate risks, we train developers in web security basics and regularly perform third-party penetration testing before launch.
In this article, we will talk about security using the example of a multilingual e-commerce service —an online store with a buyer account. The project is built on NextJS, which is part of the backend written in JS by front-end developers. This architecture requires extra vigilance regarding security, as we'll explore in the following cases.
Penetration tests are usually performed before launching a service. While there are well-known vulnerabilities that are commonly checked, effective pentests go beyond standard methodologies to uncover unique vulnerabilities.
As mentioned above, the service we are talking about is built on NextJS with the following architecture:
This architecture distributes security responsibilities between backend and frontend developers, potentially leading to vulnerabilities on both sides.
Feature: In the personal account, the user can view their current subscriptions. The data is retrieved via a POST request from the “my account” API to Umbraco, which then pulls active subscriptions from Shopify. The tester intercepted this request, examined its contents, removed the user session cookie from it, and passed an empty request body. As a result, they received all subscriptions of all users.
When developing such features, it’s crucial to consider the overall architecture of the application and how the backend and frontend communicate. Looking at the diagram above, consider the sequence of receiving subscriptions:
Separately, both the frontend and backend functioned correctly, but a security hole appeared when integrated —no one checked the user's session: the frontend thought that the session would be checked by the backend on Umbraco, and the backend thought that the frontend would check the session on NextJS.
Additionally, the Shopify API, when passed an empty email as a parameter, returns all subscriptions of all users, meaning the email parameter acts as a filter, and if it is empty, everything is returned. This combination led to the discovered problem.
You shouldn't pass email and other user data as request parameters from a client JS application. Instead, retrieve the user's email from the current user's session context, and validate this session.
The website has the ability to change the password. Testers intercepted the change password request,removed the identification cookie, and sent an empty request with only a body, containing the old and new passwords, to the backend. In one place, security allowed such a request, and checked it in another. When making a request to change the password, retrieve information about the logged-in user from identification cookies, not parameters.
Another similar case : the website has a project builder section, like favourites, where the user can arrange the products they like in project folders. Testers intercepted a request, removed the identification cookie, but left the request body, which included information on which user to create a collection for and from which products. As a result, they could update another user's favourites without authentication. None of the security levels prevented such penetration.
In both cases, the same vulnerability: data is transmitted as parameters in the API instead of being taken from the current user's session.
There may be situations on the website when a user, especially true for user portals,can create some entity that the website will display later. For example, the above-mentioned project folders. In some cases, the website allows the insertion of not only letters as a project name but also a piece of script that, for example, will steal user cookies. When an intruder does this, user cookies may appear in the alert or be sent somewhere.
One might argue: I create a project for myself, so no one except me can see this data.However, this functionality can evolve: initially, the project name display is available only to a unique user, but later, an admin role might appear.If an admin user encounters this vulnerability, their data might be compromised. Another development possibility is adding the function of sharing projects between users. The user gives the project to another, the script executes, and the cookies are stolen.
Redirects. A user navigating the site can add a product to the cart and then proceed to checkout. The peculiarity of the website implementation is that e-commerce is made on Shopify – the cart is rendered on our website, but for checkout – delivery and payment – we redirect the user to the Shopify portal.
If the user is logged in on our website, the same logged-in state must be preserved when redirecting to Shopify. In Shopify terminology, this Single-SignOn implementation is called Multipass, and you can read about it in detail here.
In short, to generate a redirect, NextJS sends a request with the URL for the redirect to the Umbraco API, which encrypts this URL with a secret key from Shopify, and substitutes the user ID needed for login during the redirect. Our Umbraco API code allows any URL to be inserted into the redirect , and URL Shopify doesn't check it during the redirect.
This is dangerous because it can lead to phishing. An intruder can prepare a phishing website that looks like your website: the first domain will be valid, but when clicking on the link, the user will be redirected to the phishing site and may trust the initial appearance, giving out their data.
Ensure that the redirect logic checks the redirect URL for a known domain before executing the redirect.
When logging into a website, the user enters a login and password, after which a request is sent to the server. Some websites do no limit the number of login attempts. In such cases, an attacker can prepare a script for password guessing and run it for a long time.
Usually, a couple of levels of protection are implemented. Login forms can be hidden under a captcha —thus, an attacker must automate captcha solving for repeated attempts.
Alternatively, an invisible Google captcha can be used, checked on the backend before further code execution. Another method is rate limiting:limit attempts from one IP, browser, etc. The simplest approach is to return an error after several login attempts from one IP address. Algorithms can also increase waiting times: the first incorrect login attempt gets a quick response, the second takes more time, and so on. If response time increases exponentially, several failed attempts will result in a long wait.
Imagine someone has hacked a user's account and started doing something on the website on their behalf. The user immediately resets the password, but the attacker remains logged in, since their session is still active.
Resetting the password should also invalidate all active sessions created for the user. Thus, if the NextJS application is responsible for creating and checking sessions, all active sessions should be reset when the user's password is reset to prevent a security hole.
To implement this logic, store the user's active sessions somewhere, since NextJS does not do this natively. In our case, we stored active sessions in the Umbraco database, and the NextJS application updated session data via the API.
In the registration form on the website, you can enter a script and HTML markup in the user's first and last name fields. Usually, the backend provides protection against SQL injections, but protection against frontend scripts is not always obvious.
Why is this dangerous? Registration data collected by the website is often inserted into the registration confirmation email. For example, “Good afternoon, UserName! Thank you for registering on our portal.”
A user can register on a third-party website on behalf of a victim, who will receive an email from a valid site with links and scripts that execute if the victim clicks the link. This problem is solved by validating input parameters during user registration.
Incorrect Google Maps API keys setup (and any other paid services and APIs allowing domain usage limits). If domain settings are incorrect, these keys can be used on other sites, and the client will pay more money. There are cases when keys were stolen, and the website owner paid thousands of dollars.
In addition to setting up restrictions for these keys, it’s crucial to optimise the number of requests to paid services.. Inefficient code can lead to excessive API calls, potentially allowing competitors to exploit your resources and increase your costs.
To limit the number of requests, consider reducing the search area and minimising requests to Google Places. A common inefficiency occurs when the map component is initialised on every page load, even when the map is located far down the page or hidden in an inactive tab. Implement lazy loading to initialise map components only when necessary.
These security considerations apply to software development in general, not just e-commerce platforms or similar projects. However, NextJS applications are particularly prone to security vulnerabilities due to the unique redistribution of responsibilities between frontend and backend tasks. By keeping these scenarios in mind, development teams can better protect themselves and their users from potential security risks.
It's easy to start working with us. Just fill the brief or call us.