Security Considerations for Web Applications and Best Practices
Table of Contents
Cross Site Scripting (XSS)
XSS could be very harmful to your site’s users because malicious code is executed in their browsers when they load your site. The malicious code not only could modify your page’s DOM or window location (e.g., via executing
window.location = "www.evil.com), but also could steal your users’ information or perform actions on their behalf.
Malicious user of your website put some malicious code into a form. The client/server does not sanitize user input and processes the malicious code (e.g., an inline script like
<script>alert('you're hacked')</script>) as if it were data. The malicious code is then stored in your web app’s server and served to other users of your site as data.
Upon page load, the malicious code is executed, which could make your web app inoperable and harm your users when the malicious code is run in their browsers. There are three types of XSS attacks: stored, reflected, and DOM based.
There are some simple things we can do to protect our web app against XSS:
1. Enable web browser’s XSS protection.
You also want this to your app’s response header:
X-XSS-Protection 1; mode=block
We can use a npm package called
helmet to add the xssFilter to request.
import helmet from ‘helmet’; app.use(helmet.xssFilter());
But Firefox does not protect you from xss with this in the header. So make sure you sanitize your input!
2. Set Content Security Policy (CSP) Header.
CSP is a security feature that web browsers offer which allows the web app to tell web browsers what should and should not be executed when rendering the website. For example, this is a basic CSP that forbids execution of inline script
Content-Security-Policy: default-src 'self';
3. Sanitize user inputs.
We can validate user input on the client side or server side. On the client side, we can display a warning message to the user that special characters such as angle brackets are not allowed and prevent the user from submitting the form to the server when there are invalid characters. On the server side, we can sanitizing user inputs ensures we don’t allow malicious user generated content to be stored in persistent storage on your server. Sanitization means replacing angle brackets, slashes, and other characters with their html entities equivalent. For example,
>. This is generally achieved using regular expressions but there are open source libraries to use for input validation and sanitization.
import validator from ‘validator’; let q = '<script>alert("you are hacked")</script>' let sanitizedQ = validator.escape(q)
<script>alert("you are hacked")</script> into:
<script>alert("you are hacked")</script>
Try out validator in the runkit playground
Or use curl to interact with the endpoint:
$ curl -L https://xiaoyunyang.runkit.io/validator-example/branches/master
There’s a famous XKCD that illustrates the danger of XSS that illustrates the first case.
<script> tags and executed by the server on the SQL database vice the browser.
Sanize all user input!
After clicking an embedded link on a web page to open a new web page in a different tab, the originating web page changes (e.g., redirects to a different page).
This hack is called Reverse Tabnabbing.
Mathias Bynens provides a really great example of reverse tabnabbing.
Web pages have a ton of links which open to another webpage from a different origin. Sometimes, these result in opening the page in another tab. This behavior is implemented in HTML using the anchor tag.
Suppose our index.html has the following line:
<a target="_blank" href="http://example.com/malicious.html"> Example site </a>
When that link is clicked,
malicious.html opens in a new tab and has access to the
window object of
index.html through the
malicious.html is from a different origin than
malicious.html has access to
window.opener.location, which can be used to automatically redirect originating webpage to a malicious website.
You should always use
rel=noopener noreferrer whenever you use
target="_blank", especially if you want to open a link from a different origin in a separate tab.
<a target="_blank" href="http://example.com/malicious.html" rel='noopener noreferrer'> Example site </a>
From the W3 Spec for
relattribute specifies the relationship between the current document and the linked document.
noopenertells the browser not to not send
window.openercontext from the originator of the link click.
noreferrertells the browser to not send an HTTP referer header if the user follows the hyperlink.
Pro-tip Use lint. In fact, I first learned about reverse tabnabbing from a lint error about the danger of using
target="_blank" in anchor without adding the
rel="noopener noreferrer" attribute. It’s always a good idea to use lint to support your development which does a lot to help you write secure, clean, and maintainable code. Popular editors like VSCode and Atom have lint support.
Cross-site Request Forgery (CSRF)
A user of your site is tricked into making a post request and your app accepts that request and performs some actions which the user does not want such as transferring money from their bank account to a hacker’s account, deleting their user account, or changing their username.
Many web apps are designed to accept post requests from authenticated users to perform some important actions such as transferring money.
To verify that the request is coming from the actual user, your app would need to check for a session id that is transmitted in the request header. Session id is stored in a cookie, which is essentially a small piece of text stored on a user’s computer by their browser.
Checking the user’s session id is a good start but not enough.
Suppose I’m a user of your web app. A hacker doesn’t know my cookie and can’t guess it but she can trick me into sending a post request to your app by making me fill out a fake form or the real form embedded in an iframe (with some CSS tricks) that submits a post request to your app without my knowledge. The second technique is known as clickjacking and iframe attack.
Because I’m the one making the request, I have the correct session id stored in my cookie. Thus, if your app only checks for session id to authenticate the origin of the request, it’s going to accept the post request.
There are few things your web app to protect your users from being tricked into sending post requests.
1. Expiring Sessions
Your app can log its users out automatically after a period of inactivity. This technique is employed by many online banking web apps.
The hacker cannot perform a CSRF attack without getting me to do something while I’m an authenticated user. If I’m not logged in to your site, then your app will not accept my post requests.
2. Token validation
The idea is we want to embed a randomly generated, un-guessable token in the form which the user uses to make the post request. When the user makes a post request using the form, the token is transmitted with the data as
X-CSRF-Token in the request header.
Each time the server sends the form form to the user’s browser, a different token is sent with the form to the browser. The server remembers what that token is and when it receives a post request, it checks to make sure the token in the request matches what it has on record.
A real life example of a CSRF token is the form from AngelList’s profile edit form:
3. CSP Header
To guard against a CSRF attack resulting from clickjacking and iframe, we can use the CSP to set a policy for
child-src ‘self’, indicating that the site must only be iframed by a page that shares the same origin, and no other.
Setting the HTTP header
X-FRAME-OPTIONS has the same effect as the CSP policy in its protection against clickhacking.
Working With Open Source Software
If you use a lot of open source software in your application, these dependencies do come with vulnerabilities you may not be aware of. Fortunately, the open source community of popular npm modules are active in identifying security vulnerabilities.
Defense in depth
1. Subscribe to security vulnerability warnings
If you keep your code on Github, Github will show a warning about a repo that contains a dependency that has a security vulnerability.
2. Keeping your dependencies up to date
If you use NPM, you can also use the following commands to find out which npm modules are outdated.
$ npm outdated
If you choose, you may update all the outdated modules with:
$ npm update
or selectively update using either one of the lines below:
$ npm install --save <package-name>@latest $ npm update <package-name>
or to a specific version:
$ npm install --save <package-name>@<version-number>
If you want to find out which packages are introducing security vulnerabilities, use the following line:
$ npm audit
Running the above command will show you some packages which you may not recognize. These are usually dependencies to the packages which you have listed in
The CLI will urge you to use
npm audit fix to fix the vulnerabilities but if you want to dig a bit deeper as to why you have these packages in the first place, and where they are used, use the following command:
$ yarn why <package-name>