Skip to main content

Web app development security best practices

Security
Concept

Overview

This document contains information regarding security best practices for developing web apps.

Authentication

Use a well-audited authentication service and client side ICP libraries

Security concern

Implementing user authentication and canister calls yourself in your web app is error prone and risky. For example, if canister calls are implemented from scratch, there may be bugs around signature creation or verification.

Recommendation

  • Consider using e.g. Internet Identity for authentication, use agent-js for making canister calls, and the auth-client for interacting with internet identity from your dapp.

  • It is of course also an option to consider alternative authentication frameworks on ICP for authentication.

Set an appropriate session timeout

Security concern

Currently, Internet Identity issues delegations with an expiry time. This expiry time can be set in the auth-client. After a delegation expires, the user has to re-authenticate. Setting a good value is a trade-off between security and usability.

Recommendation

See the OWASP recommendations. A timeout of 30 min should be set for security sensitive applications.

The auth-client supports idle timeouts. More details available here.

Don’t use fetchRootKey in agent-js in production

Security concern

agent.fetchRootKey() can be used in agent-js to fetch the root subnet threshold public key from a status call in test environments. This key is used to verify threshold signatures on certified data received through canister update calls. Using this method in a production web app gives an attacker the option to supply their own public key, invalidating all authenticity guarantees of update responses.

Recommendation

Never use agent.fetchRootKey() in production builds, only in test builds. Not calling this method will result in the hard coded root subnet public key of the mainnet being used for signature verification, which is the desired behavior in production.

Nonspecific to the Internet Computer

The best practices in this section are very general and not specific to the Internet Computer. This list is by no means complete and only lists a few very specific concerns that have led to issues in the past.

Validate input and canister data in the frontend

Security concern

Missing input validation of data from untrusted sources (e.g. users or canisters) can lead to malformed data being persisted and/or delivered back to users. This may lead to DoS, injection attacks, phishing, etc.

Recommendation

  • Perform data validation in the front end, in addition to data validation in the canister. Data validation should happen as early as possible.

  • Also validate data from canisters, especially if you don't trust the canister you are interacting with. Even if the canister is trusted, validating the data it serves is a good practice which can serve as a defense in depth mechanism e.g. in presence of security bugs in the canister or in case of a subnet compromise.

  • See the OWASP input validation cheat sheet.

Don’t load JavaScript (and other assets) from untrusted domains

Security concern

Loading untrusted JavaScript from domains other than <canister-id>.icp0.io means you completely trust that domain. Also, assets loaded from these domains (incl. <canister-id>.raw.icp0.io) will not use asset certification.

If they deliver malicious JavaScript they can take over the web app/account by e.g. reading the private key managed by agent-js from LocalStorage.

Note that also loading other assets such as CSS from untrusted domains is a security risk, see e.g. here.

Recommendation

  • Loading JavaScript and other assets from other origins should be avoided. Especially for security critical applications, you can’t assume other domains to be trustworthy.

  • Make sure all the content delivered to the browser is served and certified by the canister using asset certification. This holds in particular for any JavaScript, but also for fonts, CSS, etc.

  • Use a content security policy to prevent scripts and other content from other origins to be loaded at all. See also define security headers including a content security policy (CSP).

Define security headers including a content security policy (CSP)

Security concern

Security headers can be used to cover many security concerns, such as:

Not configuring these headers appropriately can lead to concrete security issues and missing defense-in-depth.

Recommendation

Crypto: protect key material against XSS using Web Crypto API

Security concern

Storing key material in browser storage (such as sessionStorage or localStorage) is considered unsafe because these keys can be accessed by JavaScript code. This could happen through an XSS attack or when loading untrusted scripts from other domains (see also don’t load JavaScript from untrusted domains).

Recommendation

Use Web Crypto API to hide key material from JavaScript, by using extractable=false in generateKey , see this. An example for this can be found in the encrypted notes example, see here. This makes it impossible to access the private key from JavaScript.

Use a secure web framework

Security concern

Modern web frameworks make attacks such as XSS very difficult since they safely escape / sanitize any potentially user-provided data that is rendered on a web page. Not using such a framework is risky as it is hard to avoid attacks like XSS.

Recommendation

  • Use a web framework that has a secure templating mechanism such as Svelte to avoid XSS. This is used e.g. in the NNS dapp project.

  • Don’t use insecure features of the framework, such as e.g. @html in Svelte.

Make sure the logout is effective

Security concern

If a logout action by a user is not effective, this may lead to account takeover e.g. if a shared or public device is used.

Recommendation

  • Clear all session data (especially sessionStorage and localStorage), clear IndexedDB, etc. on logout.

  • Make sure also other browser tabs showing the same origin are logged out if the logout is triggered in one tab. This does not happen automatically when agent-js is used, since agent-js keeps the private key in memory once initialized.

Use prompts to warn the user on any security critical actions, let the user explicitly confirm

Security concern

If this is not the case, a user may by accident execute some sensitive actions.

Recommendation

  • Show the user a prompt with a security warning describing the exact consequences of the action and let them confirm explicitly.

  • For applications with high security requirements, consider the use of transaction approval, i.e. using e.g. a WebAuthn device to let the user confirm certain critical actions or transactions. This is recommended e.g. when token or cycle transfers is involved. For example, using a hardware wallet in the NNS dapp achieves this.