General Best Practices
Certify query responses if they are relevant for security
The responses to query calls (as opposed to update calls) are not threshold-signed by the canister/subnet. Thus, a single malicious replica or boundary node may change the data, violating its authenticity. This is especially risky if update calls depend on the response to query calls.
All security-relevant query response data that needs authenticity guarantees (this needs to be assessed for each dApp) should be certified by the IC using certified variables. Consider using existing data structures such as certified-map. The data certification must be validated in the frontend.
Alternatively, these calls have to be issued as update calls by the caller (e.g. in agent-js), but that impacts performance: it takes a few seconds. Note that every query can also be issued as an update by the caller.
Examples are asset certification in Internet Identity, NNS dApp, or the canister signature implementation in Internet Identity.
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.
Don’t use third-party components with known vulnerabilities
Using vulnerable and outdated components is a big security risk.
Regularly check your third party components against databases of known vulnerabilities:
Rust: use cargo audit.
This should be integrated into the build process, the build should fail if there are known vulnerabilities.
Don’t use forked versions of repositories that are not maintained and may not be trustworthy.
Avoid using third party components that are not widely used and may not have had sufficient (ideally third party) review.
Pin the versions of the components you are using to avoid switching to corrupt updates automatically.
Don’t implement crypto yourself
It is easy to make mistakes when implementing cryptographic algorithms, leading to security bugs.
Use secure cryptographic schemes
Some cryptographic schemes have been broken (old TLS versions, MD5, SHA1, DES, …), or they could be so new that they have not yet been appropriately researched. Using these introduces security issues.
If you need to use crypto, only use cryptographic schemes that have not been broken and do not have known issues. Ideally use algorithms that have been standardized by e.g. NIST or IETF.
Test your code
Having small test coverage is risky, as code changes become difficult and may violate correctness and security properties, leading to bugs. It is hard to verify correctness and security properties in reviews (and security reviews) if there are no corresponding tests.
Write tests for canister implementations and frontend code, especially for security relevant properties and invariants.
Consider the DFINITY Rust guidelines on testing
For wasm-level unit testing, consider using Motoko Matchers
For long-running test scenarios, consider Motoko BigTest
Avoid test and dev code in production
It is risky to include code paths in production code that are only used for development or testing setups. If something goes wrong (and it sometimes does!), this may introduce security bugs in production.
For example, we have seen issues where the public key to verify certification was fetched from an untrusted source, since this is what is done on test networks.
Avoid test and dev code in production code whenever possible.