Web Security Checklist

A List of Web Attack Vectors and Best Practices for Protection.

I'm open to new opportunities! For a full-time role, contract position, or freelance work, reach out at talha@talhaawan.net or LinkedIn.
Web Security Checklist

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 or SameSite=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 even SameSite=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 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 implement https 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.

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.




See also

When you purchase through links on techighness.com, I may earn an affiliate commission.
We use cookies to enhance your experience. By continuing to visit this site you agree to our use of cookies. More info cookie script