by

Front-end Security Notes

Piotr Kabaciński
codequest
Published in
8 min readOct 21, 2021

--

Web application security is a broad term that touches various aspects. One of them is the front-end layer that has a significant influence in creating an overall user experience. To provide it, safety should play an important role in choosing solutions and making architecture decisions. In this article, I would like to highlight the key parts of the security vulnerabilities in the modern front-end application.

Thinking about safety starts with defining the possible threats first. Unfortunately, everything we’re not aware of or control can cause us to question our trust. In the case of a front-end app, this can be the environment specificity, data flow, dependencies, vendor assets, or user’s suspicious behavior. Let’s dive into each of them!

Be aware of the browser’s environment

The modern browser runs code in an encapsulated environment based on the Same Origin Policy. Thanks to that the application’s runtime is implicitly protected from the unwanted interactions with scripts and sites from different (possibly evil) sources. Even if we rely only on local origins, the app architecture may require disabling unwanted and risky features.

Disable opener reference in external links
A clicked anchor element can provide a direct source reference to the target value. If the link opens in a new tab (_blank) and the source session is still active, the target site can access its window object. Fortunately, this behavior should be now fixed in modern browsers like Chrome. Since not all users may have them up to date, explicit turn-off by applying rel=”noopener” attribute is recommended. You can use it with noreferrer value to protect passing the source address to the target.

Keep in mind that the opener attribute is passed when using a window.open method for the same origins as well. To disable it, override it with some falsy value if it’s not needed:

Validate postMessage target and source origins

PostMessage interface provides a connection layer between cross-origin frames, tabs, or browser windows. By design, we can listen to messages sent to the page and trigger a defined callback. If the communication is two-way and we don’t check the incoming event origin or specify the target source our message can be sent to and used by unauthorized recipients.

Regarding the safety of the sent objects, postMessage creates deep copies of them.

Control the iframes

Even though iframes can be often replaced with some other solutions, they can still be used in some cases. When it comes to using them, it’s good to remember a couple of facts:

By default, a child and parent of the same origin can access each other. Consider using the sandbox attribute to separate them and protect from such possibility. Be aware that applying it may require allowing specific features (for example inner scripts execution or forms submission) to make the iframe fully work.

Even if you don’t use iframes, your page can be used to perform a clickjacking attack. To avoid this malicious usage consider returning the X-Frame-Options header with deny value if using your page in such a context is unwanted.

Monitor dependencies and keep them up to date

Modern web apps depend on many vendor modules installed via package managers or referred via CDNs. Unfortunately, among thousands of wonderful and safe ones, there’s a risk that some may intentionally or unintentionally be infected. The story of the event stream package proves that no regular care about deps. is a terrible idea. What strategies can solve the issue then?

If for some reason you are not able to host scripts locally, consider applying Subresource Integrity Token for authorized and official CDNs and explicitly require HTTPS connection to this resource. If the provider doesn’t generate a token for them, you can always do it by yourself using command-line tools or services like ​​https://www.srihash.org/:

When using a package manager it’s good practice to apply a stage to your CI/CD process that checks the dependencies for vulnerabilities. You can achieve that by running npm audit or yarn audit. Of course, not all reported issues may be relevant to your use cases. You can whitelist them using tools like audit-ci. If you don’t use any CI approach, today’s web services like GitHub’s dependabot offer scanning dependencies features. If there are no automated tools around, you can always run the audit manually or add this command as a step in the build process.

Consider defining some criteria that could help with choosing trustworthy ones. Is there a community, a well-known organization, or a developer behind it? Is the package popular and opinionated? When was its last update? How many issues does it have? Is it maintained regularly? Even small research can help avoid some possible troubles in the future. If the package is small or you don’t need all the functionalities it provides you may think of applying the required logic by yourself instead.

Remember to update your packages often even if no crucial changes were released. Using npm update or yarn upgrade you can bump up their versions to those specified in package.json. To update to the latest ones you can use npm check updates. To do the updates regularly common practice in some companies is a “Patch day” where on the day of week or month this process takes place.

Define a Content & Features Security Policy

Content Security Policy allows us to define a white list of trusted sources our application can interact with. Thanks to that, we can avoid the consequences of XSS attacks or data leaks to unauthorized resources. CSP can scope several resources: images, scripts, forms, styles, or fonts. Inline declarations can be marked as trusted by using a dynamic nonce attribute. To verify the policy you can try out Google’s CSP Evaluator.

Features Policy can determine which resources are allowed to use the browser’s features like camera or geolocation. If any untrusted resource would want to use it we can prevent it from that.

Avoid using local/session storage for sensitive data

A very common practice is to save session tokens in local/session storage or cookies set directly by the front-end app. It’s not the best idea. They’re accessible from JavaScript code and can be exploited, for example by a self XSS attack. Front-end should just store data like user settings, application config, or dedicated tokens (for example CSRF). Session tokens (like JWT or any kind) should be established by the backend and stored as an HTTP-only cookie. They’re not available via JavaScript cookie object but are added to every request to this resource. In case of AJAX requests (using fetch), we can apply them using credentials property:

When using cookies you can limit its usage by declaring Path, Domain, and Secure attributes. They can limit its scope to specified channels. Also, you can consider using signed cookies when communicating with the backend.

Protect forms with CSRF Token

Cross-site Request Forgery tokens are dynamic values added to backend requests and renewed every page request. Thanks to that, any attempts of sending a request on behalf of the logged user could be prevented. A common practice is to store the token value in a hidden field or as an additional request header.

Validate input and output data

If the app’s feature behavior relies on user input or exposed params it can be tempting to manipulate them to trigger some unpredicted action or data scope. To protect from this kind of scenario, we should always validate if the I/O data meet our requirements. To achieve this in runtime we can use tools like joi or superstruct to create schema validation for any data — response, requests body, form submit data or query strings. The absolute must is to sanitize the received data before using it in the app, especially in the UI.

Keep the assets local

If the license allows that, a good practice is to host your assets rather than referring to external resources. Apart from not relying on other servers, you may gain more control on data exposed outside, like estimating traffic to your page via the referrals of the assets. Linked assets can be also used for some malicious behavior, like using CSS font as a text reader.

Prepare a safe build & production environment

Building and deploying an app is a key moment that should be handled with the highest care. Apart from optimizing the code, we should disable all development parts of code that could reveal some weak parts that may be used against the app itself.

Disable source maps, console.logs, or dev tools processes

In the production code, any data and state should be exposed only in the UI. Development logs or utils (like Redux dev tools) are not necessary for a production build. To debug production code better use trackers like Sentry or Bug Snag. To prevent possible self XSS attacks you can use a console to inform users about the risk of executing any code in the context of your app (just like Facebook does for example).

Lint your code

Today linters like ESLint apart from code tips offers security hints for supported browsers as well. You can extend it with some dedicated plugins.

Minify and obfuscate the code

Apart from optimization benefits, any attempts of analyzing or reverse-engineering code will be harder. Using Webpack plugins or Grunt/Gulp tasks it’s an easy process. Be careful using some online tools, though. They can smuggle unwanted code into yours.

Use HTTPS protocol and serve your app behind a CDN server

Reachability of your app is a key thing. Thanks to CDN services like Cloudflare or AWS CloudFront you can protect it from suspicious requests and attempts like DDoS attacks. To ensure sending and safely receiving data, apply SSL certificate to your domain.

Hide content from web crawlers and unwanted audience

Apply robots.txt file if some parts of your application (like the admin panel app) should not be available in popular search engines. Also, be aware that popular subdomains & endpoints like “admin”, “test”, “staging”, “api” are often scanned by bots. As an additional security layer, you can consider Basic Auth or limit connections to your app to trusted IPs whitelist.

Validate your app for security concerns

When setting up a production environment for your app, try out some tools to validate it against the most common security issues. One of the popular ones is The Mozilla Observatory.

Test your code

Writing tests has many benefits. One of them is the mindset of figuring out possible scenarios that could break the feature and should be covered by test cases. Also in the matter of security. One of the common mistakes is global declarations. Global (window) scope makes it easy to override them from outside.

Last but not least

Having constant security concerns is a great approach to solve the possible problems until they come up. In the case of the front-end, where the issues may not have the same impact as on the back-end, the main task is to prevent somebody from hacking the data and app behavior. That’s why real and efficient cooperation between those two layers is so important. Similar security strategies (for example for the data validation) should be applied to both of them. Being up to date with threats and attacks and applying the solutions from the early stage of development is a key part of responsible software engineering.

--

--