-
Notifications
You must be signed in to change notification settings - Fork 374
[WIP] Not-so-smart pallets #124
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
38 commits
Select commit
Hold shift + click to select a range
c268d91
initial commit to update e2e documentation
anishnaik 5f612aa
config option fix
anishnaik d537715
added text about etheno edge cases
anishnaik 396e5f0
docker instructions improved
anishnaik 01b39d5
Initial commit for Substrate not-so-smart
anishnaik 91da30a
Remove transfer function from mint.rs
anishnaik d1aa7bb
Adding functioning pallet-overflow.rs
anishnaik ee2fd2c
Changed comment
anishnaik c70c90b
Add verify first, write list pallet
anishnaik 50e211c
Add pallets-bad-weight.rs
anishnaik 48cf018
Initial commit for validate unsigned transactions
anishnaik 48ba65f
Adding example where pallet panics instead of proper error handling
anishnaik 13fe9ac
Code cleanup for pallet-dont-panic.rs
anishnaik 64bdd33
Comment update in pallet-dont-panic.rs
anishnaik 1ea500f
Bad randomness example
anishnaik f51030a
Update folder names with underscores instead of hyphens
anishnaik 9579c7f
README for arithmetic_overflow complete
anishnaik 0aafb46
README for dont_panic complete
anishnaik bb64f06
README for weights_and_fees complete
anishnaik a7a95c8
Renamed runtime_storage folder to verify_first folder
anishnaik 813d9ca
Add link to README for weights_and_fees
anishnaik 97ec3bf
README for verify_first complete
anishnaik 89e542d
Rename pallet-bad-oracle to pallet-bad-unsigned
anishnaik f402901
README for validate_unsigned complete
anishnaik 6003666
Add source for pallet-bad-unsigned.rs
anishnaik 538a205
Rename pallet-lottery.rs to pallet-bad-lottery.rs
anishnaik 1d76af8
README for randomness complete
anishnaik b3c5fbf
README for NSS pallets
anishnaik bafac13
Minor copyediting changes in all READMEs
anishnaik 67abdec
Added bad origins pallet and README
anishnaik d0bc881
Updated NSS pallet README
anishnaik df5dfe9
Fix bad origins README
anishnaik 5097302
Fix bad randomness README
anishnaik fb19932
Fix validate unsigned README
anishnaik a4fc1bd
Fix weights and fees README
anishnaik 49e23bc
minor doc changes
5e84c96
fix note
anishnaik 2f78513
fix weight function
anishnaik File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,31 @@ | ||
| # (Not So) Smart Pallets | ||
|
|
||
| This repository contains examples of common Substrate pallet vulnerabilities. Use Not So Smart Pallets to learn about Substrate vulnerabilities, as a reference when performing security reviews, and as a benchmark for security and analysis tools. | ||
|
|
||
| ## Features | ||
|
|
||
| Each _Not So Smart Pallet_ includes a standard set of information: | ||
|
|
||
| * Description of the vulnerability type | ||
| * Attack scenarios to exploit the vulnerability | ||
| * Recommendations to eliminate or mitigate the vulnerability | ||
| * A mock pallet that exhibits the flaw | ||
| * References to third-party resources with more information | ||
|
|
||
| ## Vulnerabilities | ||
|
|
||
| | Not So Smart Pallet | Description | | ||
| | --- | --- | | ||
| | [Arithmetic overflow](./arithmetic_overflow/README.md) | Integer overflow due to incorrect use of arithmetic operators | | ||
| | [Don't panic!](./dont_panic/README.md) | System panics create a potential DoS attack vector | | ||
| | [Weights and fees](./weights_and_fees/README.md) | Incorrect weight calculations can create a potential DoS attack vector | | ||
| | [Verify first](./verify_first/README.md) | Verify first, write last | | ||
| | [Unsigned transaction validation](./validate_unsigned/README.md) | Insufficient validation of unsigned transactions | | ||
| | [Bad randomness](./randomness/README.md) | Unsafe sources of randomness in Substrate | | ||
| | [Bad origin](./origins/README.md) | Incorrect use of call origin can lead to bypassing access controls | | ||
|
|
||
| ## Credits | ||
|
|
||
| These examples are developed and maintained by [Trail of Bits](https://www.trailofbits.com/). | ||
|
|
||
| If you have questions, problems, or just want to learn more, then join the #ethereum channel on the [Empire Hacking Slack](https://empireslacking.herokuapp.com/) or [contact us](https://www.trailofbits.com/contact/) directly. |
44 changes: 44 additions & 0 deletions
44
not-so-smart-contracts/substrate/arithmetic_overflow/README.md
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,44 @@ | ||
| # Arithmetic overflow | ||
|
|
||
| Arithmetic overflow in Substrate occurs when arithmetic operations are performed using primitive operations instead of specialized functions that check for overflow. When a Substrate node is compiled in `debug` mode, integer overflows will cause the program to panic. However, when the node is compiled in `release` mode (e.g. `cargo build --release`), Substrate will perform two's complement wrapping. A production-ready node will be compiled in `release` mode, which makes it vulnerable to arithmetic overflow. | ||
|
|
||
| # Example | ||
| In the [`pallet-overflow`](./pallet-overflow.rs) pallet, notice that the `transfer` function sets `update_sender` and `update_to` using primitive arithmetic operations. | ||
|
|
||
| ```rust | ||
| /// Allow minting account to transfer a given balance to another account. | ||
| /// | ||
| /// Parameters: | ||
| /// - `to`: The account to receive the transfer. | ||
| /// - `amount`: The amount of balance to transfer. | ||
| /// | ||
| /// Emits `Transferred` event when successful. | ||
| #[pallet::weight(10_000)] | ||
| pub fn transfer( | ||
| origin: OriginFor<T>, | ||
| to: T::AccountId, | ||
| amount: u64, | ||
| ) -> DispatchResultWithPostInfo { | ||
| let sender = ensure_signed(origin)?; | ||
| let sender_balance = Self::get_balance(&sender); | ||
| let receiver_balance = Self::get_balance(&to); | ||
|
|
||
| // Calculate new balances. | ||
| let update_sender = sender_balance - amount; | ||
| let update_to = receiver_balance + amount; | ||
| [...] | ||
| } | ||
| ``` | ||
|
|
||
| The sender of the extrinsic can exploit this vulnerability by causing `update_sender` to underflow, which artificially inflates their balance. | ||
|
|
||
| **Note**: Aside from the stronger mitigations mentioned below, a check to make sure that `sender` has at least `amount` balance would have also prevented an underflow. | ||
|
|
||
| # Mitigations | ||
| - Use `checked` or `saturating` functions for arithmetic operations. | ||
| - [`CheckedAdd` trait](https://docs.rs/num/0.4.0/num/traits/trait.CheckedAdd.html) | ||
| - [`Saturating` trait](https://docs.rs/num/0.4.0/num/traits/trait.Saturating.html) | ||
|
|
||
| # References | ||
| - https://doc.rust-lang.org/book/ch03-02-data-types.html#integer-overflow | ||
| - https://docs.substrate.io/reference/how-to-guides/basics/use-helper-functions/ |
113 changes: 113 additions & 0 deletions
113
not-so-smart-contracts/substrate/arithmetic_overflow/pallet-overflow.rs
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,113 @@ | ||
| #![cfg_attr(not(feature = "std"), no_std)] | ||
|
|
||
| pub use pallet::*; | ||
| #[frame_support::pallet] | ||
| pub mod pallet { | ||
| use frame_support::{dispatch::DispatchResultWithPostInfo, pallet_prelude::*}; | ||
| use frame_system::pallet_prelude::*; | ||
|
|
||
| /// Pallet configuration | ||
| #[pallet::config] | ||
| pub trait Config: frame_system::Config { | ||
| /// Because this pallet emits events, it depends on the runtime's definition of an event. | ||
| type Event: From<Event<Self>> + IsType<<Self as frame_system::Config>::Event>; | ||
| } | ||
|
|
||
| #[pallet::pallet] | ||
| #[pallet::generate_store(pub(super) trait Store)] | ||
| pub struct Pallet<T>(_); | ||
|
|
||
| // DEFAULT total supply of tokens | ||
| #[pallet::type_value] | ||
| pub(super) fn TotalSupplyDefaultValue<T: Config>() -> u64 { | ||
| 1000 | ||
| } | ||
|
|
||
| // Data structure that holds the total supply of tokens | ||
| #[pallet::storage] | ||
| #[pallet::getter(fn total_supply)] | ||
| pub(super) type TotalSupply<T: Config> = | ||
| StorageValue<_, u64, ValueQuery, TotalSupplyDefaultValue<T>>; | ||
|
|
||
| // Data structure that holds whether or not the pallet's init() function has been called | ||
| #[pallet::storage] | ||
| #[pallet::getter(fn is_init)] | ||
| pub(super) type Init<T: Config> = StorageValue<_, bool, ValueQuery>; | ||
|
|
||
| /// Storage item for balances to accounts mapping. | ||
| #[pallet::storage] | ||
| #[pallet::getter(fn get_balance)] | ||
| pub(super) type BalanceToAccount<T: Config> = StorageMap< | ||
| _, | ||
| Blake2_128Concat, | ||
| T::AccountId, | ||
| u64, | ||
| ValueQuery | ||
| >; | ||
|
|
||
| /// Token mint can emit two Event types. | ||
| #[pallet::event] | ||
| #[pallet::generate_deposit(pub(super) fn deposit_event)] | ||
| pub enum Event<T: Config> { | ||
| /// Token was initialized by user | ||
| Initialized(T::AccountId), | ||
| /// Tokens were successfully transferred between accounts. [from, to, value] | ||
| Transferred(T::AccountId, T::AccountId, u64), | ||
| } | ||
|
|
||
| #[pallet::hooks] | ||
| impl<T: Config> Hooks<BlockNumberFor<T>> for Pallet<T> {} | ||
|
|
||
| #[pallet::error] | ||
| pub enum Error<T> { | ||
| /// Attempted to initialize the token after it had already been initialized. | ||
| AlreadyInitialized, | ||
| } | ||
|
|
||
| #[pallet::call] | ||
| impl<T:Config> Pallet<T> { | ||
| /// Initialize the token | ||
| /// Transfers the total_supply amount to the caller | ||
| /// If init() has already been called, throw AlreadyInitialized error | ||
| #[pallet::weight(10_000)] | ||
| pub fn init(origin: OriginFor<T>) -> DispatchResultWithPostInfo { | ||
| let sender = ensure_signed(origin)?; | ||
| ensure!(!Self::is_init(), <Error<T>>::AlreadyInitialized); | ||
|
|
||
| <BalanceToAccount<T>>::insert(sender, Self::total_supply()); | ||
|
|
||
| Init::<T>::put(true); | ||
| Ok(().into()) | ||
| } | ||
|
|
||
| /// Allow minting account to transfer a given balance to another account. | ||
| /// | ||
| /// Parameters: | ||
| /// - `to`: The account to receive the transfer. | ||
| /// - `amount`: The amount of balance to transfer. | ||
| /// | ||
| /// Emits `Transferred` event when successful. | ||
| #[pallet::weight(10_000)] | ||
| pub fn transfer( | ||
| origin: OriginFor<T>, | ||
| to: T::AccountId, | ||
| amount: u64, | ||
| ) -> DispatchResultWithPostInfo { | ||
| let sender = ensure_signed(origin)?; | ||
| let sender_balance = Self::get_balance(&sender); | ||
| let receiver_balance = Self::get_balance(&to); | ||
|
|
||
| // Calculate new balances. | ||
| let update_sender = sender_balance - amount; | ||
| let update_to = receiver_balance + amount; | ||
|
|
||
| // Update both accounts storage. | ||
| <BalanceToAccount<T>>::insert(&sender, update_sender); | ||
| <BalanceToAccount<T>>::insert(&to, update_to); | ||
|
|
||
| // Emit event. | ||
| Self::deposit_event(Event::Transferred(sender, to, amount)); | ||
| Ok(().into()) | ||
| } | ||
| } | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,39 @@ | ||
| # Don't Panic! | ||
|
|
||
| Panics occur when the node enters a state that it cannot handle and stops the program / process instead of trying to proceed. Panics can occur for a large variety of reasons such as out-of-bounds array access, incorrect data validation, type conversions, and much more. A well-designed Substrate node must NEVER panic! If a node panics, it opens up the possibility for a denial-of-service (DoS) attack. | ||
|
|
||
| # Example | ||
| In the [`pallet-dont-panic`](./pallet-dont-panic.rs) pallet, the `find_important_value` dispatchable checks to see if `useful_amounts[0]` is greater than `1_000`. If so, it sets the `ImportantVal` `StorageValue` to the value held in `useful_amounts[0]`. | ||
|
|
||
| ```rust | ||
| /// Do some work | ||
| /// | ||
| /// Parameters: | ||
| /// - `useful_amounts`: A vector of u64 values in which there is a important value. | ||
| /// | ||
| /// Emits `FoundVal` event when successful. | ||
| #[pallet::weight(10_000)] | ||
| pub fn find_important_value( | ||
| origin: OriginFor<T>, | ||
| useful_amounts: Vec<u64>, | ||
| ) -> DispatchResultWithPostInfo { | ||
| let sender = ensure_signed(origin)?; | ||
|
|
||
| ensure!(useful_amounts[0] > 1_000, <Error<T>>::NoImportantValueFound); | ||
|
|
||
| // Found the important value | ||
| ImportantValue::<T>::put(&useful_amounts[0]); | ||
| [...] | ||
| } | ||
| ``` | ||
|
|
||
| However, notice that there is no check before the array indexing to see whether the length of `useful_amounts` is greater than zero. Thus, if `useful_amounts` is empty, the indexing will cause an array out-of-bounds error which will make the node panic. Since the `find_important_value` function is callable by anyone, an attacker can set `useful_amounts` to an empty array and spam the network with malicious transactions to launch a DoS attack. | ||
|
|
||
| # Mitigations | ||
| - Write non-throwing Rust code (e.g. prefer returning [`Result`](https://paritytech.github.io/substrate/master/frame_support/dispatch/result/enum.Result.html) types, use [`ensure!`](https://paritytech.github.io/substrate/master/frame_support/macro.ensure.html), etc.). | ||
| - Proper data validation of all input parameters is crucial to ensure that an unexpected panic does not occur. | ||
| - A thorough suite of unit tests should be implemented. | ||
| - Fuzz testing (e.g. using [`test-fuzz`](https://github.com/trailofbits/test-fuzz)) can aid in exploring more of the input space. | ||
|
|
||
| # References | ||
| - https://docs.substrate.io/main-docs/build/events-errors/#errors | ||
68 changes: 68 additions & 0 deletions
68
not-so-smart-contracts/substrate/dont_panic/pallet-dont-panic.rs
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,68 @@ | ||
| #![cfg_attr(not(feature = "std"), no_std)] | ||
|
|
||
| pub use pallet::*; | ||
|
|
||
| #[frame_support::pallet] | ||
| pub mod pallet { | ||
| use frame_support::{dispatch::DispatchResultWithPostInfo, pallet_prelude::*}; | ||
| use frame_system::pallet_prelude::*; | ||
| use sp_std::prelude::*; | ||
|
|
||
| /// Pallet configuration | ||
| #[pallet::config] | ||
| pub trait Config: frame_system::Config { | ||
| /// Because this pallet emits events, it depends on the runtime's definition of an event. | ||
| type Event: From<Event<Self>> + IsType<<Self as frame_system::Config>::Event>; | ||
| } | ||
|
|
||
| #[pallet::pallet] | ||
| #[pallet::without_storage_info] | ||
| #[pallet::generate_store(pub(super) trait Store)] | ||
| pub struct Pallet<T>(_); | ||
|
|
||
| /// Storage item for holding an ImportantValue | ||
| #[pallet::storage] | ||
| #[pallet::getter(fn get_val)] | ||
| pub(super) type ImportantValue<T: Config> = StorageValue<_, u64, ValueQuery>; | ||
|
|
||
| #[pallet::event] | ||
| #[pallet::generate_deposit(pub(super) fn deposit_event)] | ||
| pub enum Event<T: Config> { | ||
| /// Emit after val is found | ||
| FoundVal(T::AccountId, u64), | ||
| } | ||
|
|
||
| #[pallet::hooks] | ||
| impl<T: Config> Hooks<BlockNumberFor<T>> for Pallet<T> {} | ||
|
|
||
| #[pallet::error] | ||
| pub enum Error<T> { | ||
| NoImportantValueFound, | ||
| } | ||
|
|
||
| #[pallet::call] | ||
| impl<T:Config> Pallet<T> { | ||
| /// Find important value | ||
| /// | ||
| /// Parameters: | ||
| /// - `useful_amounts`: A vector of u64 values in which there is a important value. | ||
| /// | ||
| /// Emits `FoundVal` event when successful. | ||
| #[pallet::weight(10_000)] | ||
| pub fn find_important_value( | ||
| origin: OriginFor<T>, | ||
| useful_amounts: Vec<u64>, | ||
| ) -> DispatchResultWithPostInfo { | ||
| let sender = ensure_signed(origin)?; | ||
|
|
||
| ensure!(useful_amounts[0] > 1_000, <Error<T>>::NoImportantValueFound); | ||
|
|
||
| // Found the important value | ||
| ImportantValue::<T>::put(&useful_amounts[0]); | ||
|
|
||
| // Emit event | ||
| Self::deposit_event(Event::FoundVal(sender, useful_amounts[0])); | ||
| Ok(().into()) | ||
| } | ||
| } | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,59 @@ | ||
| # Origins | ||
|
|
||
| The origin of a call tells a dispatchable function where the call has come from. Origins are a way to implement access controls in the system. | ||
|
|
||
| There are three types of origins that can used in the runtime: | ||
|
|
||
| ```rust | ||
| pub enum RawOrigin<AccountId> { | ||
| Root, | ||
| Signed(AccountId), | ||
| None, | ||
| } | ||
| ``` | ||
|
|
||
| Outside of the out-of-box origins, custom origins can also be created that are catered to a specific runtime. The primary use case for custom origins is to configure privileged access to dispatch calls in the runtime, outside of `RawOrigin::Root`. | ||
|
|
||
| Using privileged origins, like `RawOrigin::Root` or custom origins, can lead to access control violations if not used correctly. It is a common error to use `ensure_signed` in place of `ensure_root` which would allow any user to bypass the access control placed by using `ensure_root`. | ||
|
|
||
| # Example | ||
| In the [`pallet-bad-origin`](./pallet-bad-origin.rs) pallet, there is a `set_important_val` function that should be only callable by the `ForceOrigin` _custom_ origin type. This custom origin allows the pallet to specify that only a specific account can call `set_important_val`. | ||
|
|
||
| ```rust | ||
| #[pallet::call] | ||
| impl<T:Config> Pallet<T> { | ||
| /// Set the important val | ||
| /// Should be only callable by ForceOrigin | ||
| #[pallet::weight(10_000)] | ||
| pub fn set_important_val( | ||
| origin: OriginFor<T>, | ||
| new_val: u64 | ||
| ) -> DispatchResultWithPostInfo { | ||
| let sender = ensure_signed(origin)?; | ||
| // Change to new value | ||
| <ImportantVal<T>>::put(new_val); | ||
|
|
||
| // Emit event | ||
| Self::deposit_event(Event::ImportantValSet(sender, new_val)); | ||
|
|
||
| Ok(().into()) | ||
| } | ||
| } | ||
| ``` | ||
| However, the `set_important_val` is using `ensure_signed`; this allows any account to set `ImportantVal`. To allow only the `ForceOrigin` to call `set_important_val` the following change can be made: | ||
|
|
||
| ```rust | ||
| T::ForceOrigin::ensure_origin(origin.clone())?; | ||
| let sender = ensure_signed(origin)?; | ||
| ``` | ||
|
|
||
| # Mitigations | ||
| - Ensure that the correct access controls are placed on privileged functions. | ||
anishnaik marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| - Develop user documentation on all risks associated with the system, including those associated with privileged users. | ||
| - A thorough suite of unit tests that validates access controls is crucial. | ||
|
|
||
| # References | ||
| - https://docs.substrate.io/main-docs/build/origins/ | ||
| - https://docs.substrate.io/tutorials/work-with-pallets/specify-the-origin-for-a-call/ | ||
| - https://paritytech.github.io/substrate/master/pallet_sudo/index.html# | ||
| - https://paritytech.github.io/substrate/master/pallet_democracy/index.html | ||
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.