
I have compiled a list of possible techniques attackers use to exploit vulnerabilities in your system. As a web developer, it is good to be mindful of the common patterns and follow the best practices to guard against the attacks.
A special shout out to Steve Kinney whose Frontend Masters course on web security was a beneficial material in compiling this list.
Let’s begin.
Common Attack Vectors With Their Solutions
Cross-Site Request Forgery (CSRF)
Since you’re already logged in, a malicious site could call the backend/API of your system after guessing the right parameters, and carry out actions on your behalf. It doesn’t need JavaScript or access to your site.
Solution
- Enable reasonable CORS protection (but remember, CORS does not prevent CSRF because browsers will send cookies with simple cross-origin form submissions).
- Add CSRF token for verification.
- Use
SameSite=Strict
orSameSite=Lax
cookies. - Never implement any Create, Update, or Delete action through a GET request. For example, a malicious user could use:
<a href="https://yourbank.com/delete-account">Click me!</a>
, in which case evenSameSite=Lax
won’t help.
Cross-Site Scripting (XSS)
A malicious user can store or inject a harmful script (for example, through a comment, form input, or URL), which then gets rendered in other users' browsers, when they visit the page where the content is rendered. This can give the attacker access to the victim’s browser environment, potentially leaking sensitive information.
Solution
- Sanitize and escape the input and output of a non-trusted source. Do that at input on the front end, during storage in the database on the backend, and at the output when rendering to the browser, using standard and well-maintained libraries from the community, such as DOMPurify.
- Use restrictive Content Security Policy (CSP) to control which scripts, styles, and resources can be loaded, reducing the impact of any injected scripts
Cookie Hijacking
Cookie Hijacking occurs when the malicious actor gains unauthorized access to a user’s cookie (such as through XSS or a man-in-the-middle attack). The whole session and sensitive information of the user are being compromised, and the attacker now can impersonate the user and perform actions on their behalf.
Solution:
- Use
httpOnly: true
for cookies so that they’re not accessible through JavaScript. - Use the
signed: true
setting to cryptographically sign the cookie so that if tampered with, the request can be discarded. - Use
secure: true
to ensure that cookies are set on secure connections only. This means that you will need to implementhttps
for your site/API, which is a best practice. - To obfuscate the cookie details, and to be able to kill the session remotely, use randomly generated
sessionId
. - See the Cross Site Scripting and Cross Site Request Forgery sections and their solutions to further ensure cookie hijacking is less likely.
Man-in-the-Middle (MITM) Attack
If the request and response from the browser are not encrypted, anyone can sniff the packets and see what content is being passed around including sensitive information like email password for sign-in. This is more likely when you’re using a publicly available internet in places such as coffee shops.
Solution
Always use secure https
instead of http
, be it a web app or APIs.
Command Line Injection
You have implemented something on your backend that accepts a command from the user, such as ls
. A malicious user could send some dangerous command that could damage your server, for example rm -rf ./
Solution:
Never expect direct commands from the client. Even if you want to run system operations use standard libraries and their standard methods.
Remote Code Execution
If you have allowed the user to send a piece of code that will be executed dynamically on your backend, you have a similar problem to command line injection. The user could potentially run a nefarious script (such as bash, or javascript with eval) that can do huge damage.
Solution:
Don’t allow a remote code to be executed on your system. Even if it’s needed, highly restrict it and sanitize it to only allow whitelisted operations to exist in that dynamic code.
SQL Injection
Using the incoming request as is without proper checking in your SQL code could lead to unauthorized read or write operations from a malicious user, such as reading all the users and their sensitive information.
Solution:
Always use ORMs or standard SQL libraries (for raw SQL), because they come with complete protection of SQL injection and won’t let you commit that mistake easily.
File Upload Vulnerability
You have implemented a file upload on your system. You intended to get the users to upload images only, so you put all the proper checks on the frontend and didn’t bother to verify the type of the incoming file on the server. A malicious user can bypass the client restrictions and send a file that can potentially introduce a virus to your system or bring down your system any other way.
Solution:
Be explicit about the type, extensions, and size of the incoming files on your system. Having these checks on the client is great as a first line of defense but don’t bank on them.
Privilage Escaltion
Much like SQL injection, when you take the incoming request of the user as is (or destructure the parameters) and pass it off to your NoSQL or SQL query, and the parameter happens to be the allowed field in the schema, you might open your system up for privilege escalation attack. The user of the system could guess the schema, pass that value in the request, and upgrade their role/permission in the system, giving them access to all the information that is accessible to that role (like admin).
Solution:
- Use DTOs (such as in NestJS), JSON schema validator, yup, etc. to disallow unlisted params (return error if the extra parameter exists). No additional and unwanted parameter should make it to your business logic.
- Even with the protection in place, avoid passing the request parameters as is to your methods. Always choose the required parameters explicitly and work with them. This also helps with documentation and visibility into your code working and logic.
For more detail, check my post on privilege escalation in NoSQL and SQL.
PostMessage
Your web app is sending or receiving messages through the browser’s postMessage
without any checks in place. This could leak sensitive information about your user to a malicious site.
Solution:
- Always narrow down the scope of the iframe with a Content Security Policy. Never send the
postMessage
to wildcard domain (*) and be specific. - Similarly, on receiving the message, ensure the origin is the allowed one, and sanitize the message before proceeding with it.
Click Jacking
A malicious website could use a genuine website as an iframe and style it such that its content is hidden beneath its seemingly harmless button. If you click on the button the click could propagate to the underlying button of the genuine website, performing some sensitive operation (such as “Send Amount”). Since the user is actually logged in to the original site, the operation should be performed.
Solution:
Strong Content Security Policy. Don’t let another site put your sensitive app as an iframe.
Tabnabbing
A user is on a trusted website A. The website has shared a link, which they thought was harmless. The user clicks on the link, which opens a new tab, which has another site B. The user is spending time on this new site B when a script in this site deceivingly changes the first tab and redirects it from A to a phishing site (a replica of some well-known app like Twitter). The user, not attentive enough to the details like the address bar with a fake domain, might think they were logged out of the site because of inactivity and give in their credentials to this phishing site.
Solution:
Although most browsers have already taken care of tab nabbing by not allowing parent reference and cross-domain redirects in the new tab. Still, to keep older browsers and users in mind, it’s always a good practice to include "noopener norefferer"
check alongside target="blank"
.
I have explained tabnabbing and its effectiveness in detail here.
Rainbow Attack
Attackers have billions of pre-computed and two-way password/hash mappings in tables (called Rainbow Tables) based on common hashing algorithms (for example SHA-256). This tells them that “password123” would be a certain hash and that hash will have the password “password123”. Now, if the attacker somehow got hold of your database, they would just need to look at all the hashed passwords from the stolen database in their Rainbow Table and get the passwords of the users.
Solution:
Always put per-user salt while hashing the passwords. With salt, any precomputed Rainbow Table is effectively useless, because two same passwords will result in different hashes.
Distributed Denial of Service Attack (DDoS)
Attackers with a lot of resources could send a huge number of requests in a distributed way to your website or API, making your system slow/crash and unusable for genuine users.
Solution: Use services like Cloudflare that intelligently protect your servers from DDoS attacks. Additionally, use services like AWS API Gateway with API Keys to ensure that only authorized requests make their way to your servers (or Lambda functions) to save your compute bill. Essentially, try to run any compute only for a valid request to save resources.
Zero-Day Vulnerability
Your system is showing some information about a technology that the attacker can use to their advantage if there’s news of vulnerability found in that technology. For example, you must’ve seen powered by Express.js or nginx in the browser developer tools in the header section. They also have versions mentioned sometimes. Now, say, a vulnerability is discovered with the particular version of Express you have been using in your stack. The attackers have the list of all the sites using that version of Express, including yours. They can act quickly before you can, and exploit the vulnerability. Compare that with a site that uses the same version of Express, but never publicized it. That site will be safer than yours on zero-day.
Solution:
As a general rule, hide (or even obfuscate) as many details about your stack as possible.
Additional Best Practices
Secure Environment Variables
- Never hardcode your secrets in the code, nor put them in frontend environment variables (unless it has a secret option like Next.js). Keep them in backend environment variables, and load them with the process.
Upgrade Libraries
- Both on the backend and frontend, try to keep the libraries updated to at least the latest patch version (better yet, the latest major version). Security vulnerabilities are often found in third-party libraries whose authors then release patches. Auditing your libraries regularly and updating the critical ones should be a common practice.
Keep the Logs
Ensure that the system has a reasonable logging in place, which you should observe frequently for any inconsistencies. This is to ensure that if anything nefarious does happen from the attackers you at least know from the logs what happened and at what time, for proper auditing and fixing.
Observe the Principle of Least Privilege
For any kind of access in your whole stack, you should ensure that only the person who needs a particular access is given it.
Avoid Leaking Unnecessary Information
The user should know as little as possible about your internal system. You should not delve into the details that might provide information to the attacker, especially sensitive information. For example, you should never tell the user if their username or password is incorrect. Just inform them of incorrect credentials so that they don’t have any information to work with. Use proper status codes for errors for good contracts and communication with the client, but only share relevant information. This also includes stack traces, especially from the backend. Never let them leak to the client.
Note that you should share important information such that “the x should be a string”, or “you cannot have additional parameters ‘y’ and ‘z’”, as discussed in the Privilege Escalation section.
Store JWT Access Token in Cookie
Since localStorage
or sessionStorage
can be read through JavaScript, we’re better off storing the JWT access token in the cookie, with httpOnly: true
so that it’s safe and unreadable if the user’s window is compromised.
Use Short Duration JWT Access Token
Keep the Time To Live (TTL) of the JWT access token optimally short (about 15 minutes), so that even if the token is stolen the attack window is small and the damage is minimal. Issue a refresh token with a longer time to live which the client should use to get a new access token once the previous one is expired.