Skip to content

Commit 7d5cc5a

Browse files
anishnaikNatalie Chin
andauthored
[WIP] Not-so-smart pallets (#124)
* initial commit to update e2e documentation * config option fix * added text about etheno edge cases * docker instructions improved * Initial commit for Substrate not-so-smart 1. Added a pallet that does not comply with Verify first, write last principle. 2. Added a pallet that has an integer overflow / underflow 3. Added README.md for both pallets with almost no content * Remove transfer function from mint.rs * Adding functioning pallet-overflow.rs 1. Renamed transfer.rs to pallet-overflow.rs to comply with substrate naming convention 2. pallet-overflow.rs will compile in substrate-node-template and the bug is exploitable * Changed comment TotalSupplyDefaultValue is not the actual total supply but instead the default one. So changed comment to note that accordingly. * Add verify first, write list pallet This pallet is no longer insecure after polkadot-v0.9.25. However, it is still considered best practice so I'm including it in here. * Add pallets-bad-weight.rs This pallet demonstrates the difficulty in creating proper weight functions. If the length of useful_amounts is zero, the weight for DoWork is zero. This also demonstrates the need for benchmarking. * Initial commit for validate unsigned transactions * Adding example where pallet panics instead of proper error handling * Code cleanup for pallet-dont-panic.rs Decided to add an error type so that `ensure!()` can be used to cause the out-of-bounds check * Comment update in pallet-dont-panic.rs * Bad randomness example * Update folder names with underscores instead of hyphens * README for arithmetic_overflow complete * README for dont_panic complete * README for weights_and_fees complete * Renamed runtime_storage folder to verify_first folder * Add link to README for weights_and_fees * README for verify_first complete * Rename pallet-bad-oracle to pallet-bad-unsigned * README for validate_unsigned complete * Add source for pallet-bad-unsigned.rs * Rename pallet-lottery.rs to pallet-bad-lottery.rs * README for randomness complete * README for NSS pallets * Minor copyediting changes in all READMEs * Added bad origins pallet and README * Updated NSS pallet README * Fix bad origins README * Fix bad randomness README * Fix validate unsigned README * Fix weights and fees README * minor doc changes * fix note * fix weight function Co-authored-by: Natalie Chin <natalie.chin@trailofbits.com>
1 parent 9ba7192 commit 7d5cc5a

File tree

15 files changed

+1111
-0
lines changed

15 files changed

+1111
-0
lines changed
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
# (Not So) Smart Pallets
2+
3+
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.
4+
5+
## Features
6+
7+
Each _Not So Smart Pallet_ includes a standard set of information:
8+
9+
* Description of the vulnerability type
10+
* Attack scenarios to exploit the vulnerability
11+
* Recommendations to eliminate or mitigate the vulnerability
12+
* A mock pallet that exhibits the flaw
13+
* References to third-party resources with more information
14+
15+
## Vulnerabilities
16+
17+
| Not So Smart Pallet | Description |
18+
| --- | --- |
19+
| [Arithmetic overflow](./arithmetic_overflow/README.md) | Integer overflow due to incorrect use of arithmetic operators |
20+
| [Don't panic!](./dont_panic/README.md) | System panics create a potential DoS attack vector |
21+
| [Weights and fees](./weights_and_fees/README.md) | Incorrect weight calculations can create a potential DoS attack vector |
22+
| [Verify first](./verify_first/README.md) | Verify first, write last |
23+
| [Unsigned transaction validation](./validate_unsigned/README.md) | Insufficient validation of unsigned transactions |
24+
| [Bad randomness](./randomness/README.md) | Unsafe sources of randomness in Substrate |
25+
| [Bad origin](./origins/README.md) | Incorrect use of call origin can lead to bypassing access controls |
26+
27+
## Credits
28+
29+
These examples are developed and maintained by [Trail of Bits](https://www.trailofbits.com/).
30+
31+
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.
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
# Arithmetic overflow
2+
3+
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.
4+
5+
# Example
6+
In the [`pallet-overflow`](./pallet-overflow.rs) pallet, notice that the `transfer` function sets `update_sender` and `update_to` using primitive arithmetic operations.
7+
8+
```rust
9+
/// Allow minting account to transfer a given balance to another account.
10+
///
11+
/// Parameters:
12+
/// - `to`: The account to receive the transfer.
13+
/// - `amount`: The amount of balance to transfer.
14+
///
15+
/// Emits `Transferred` event when successful.
16+
#[pallet::weight(10_000)]
17+
pub fn transfer(
18+
origin: OriginFor<T>,
19+
to: T::AccountId,
20+
amount: u64,
21+
) -> DispatchResultWithPostInfo {
22+
let sender = ensure_signed(origin)?;
23+
let sender_balance = Self::get_balance(&sender);
24+
let receiver_balance = Self::get_balance(&to);
25+
26+
// Calculate new balances.
27+
let update_sender = sender_balance - amount;
28+
let update_to = receiver_balance + amount;
29+
[...]
30+
}
31+
```
32+
33+
The sender of the extrinsic can exploit this vulnerability by causing `update_sender` to underflow, which artificially inflates their balance.
34+
35+
**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.
36+
37+
# Mitigations
38+
- Use `checked` or `saturating` functions for arithmetic operations.
39+
- [`CheckedAdd` trait](https://docs.rs/num/0.4.0/num/traits/trait.CheckedAdd.html)
40+
- [`Saturating` trait](https://docs.rs/num/0.4.0/num/traits/trait.Saturating.html)
41+
42+
# References
43+
- https://doc.rust-lang.org/book/ch03-02-data-types.html#integer-overflow
44+
- https://docs.substrate.io/reference/how-to-guides/basics/use-helper-functions/
Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
#![cfg_attr(not(feature = "std"), no_std)]
2+
3+
pub use pallet::*;
4+
#[frame_support::pallet]
5+
pub mod pallet {
6+
use frame_support::{dispatch::DispatchResultWithPostInfo, pallet_prelude::*};
7+
use frame_system::pallet_prelude::*;
8+
9+
/// Pallet configuration
10+
#[pallet::config]
11+
pub trait Config: frame_system::Config {
12+
/// Because this pallet emits events, it depends on the runtime's definition of an event.
13+
type Event: From<Event<Self>> + IsType<<Self as frame_system::Config>::Event>;
14+
}
15+
16+
#[pallet::pallet]
17+
#[pallet::generate_store(pub(super) trait Store)]
18+
pub struct Pallet<T>(_);
19+
20+
// DEFAULT total supply of tokens
21+
#[pallet::type_value]
22+
pub(super) fn TotalSupplyDefaultValue<T: Config>() -> u64 {
23+
1000
24+
}
25+
26+
// Data structure that holds the total supply of tokens
27+
#[pallet::storage]
28+
#[pallet::getter(fn total_supply)]
29+
pub(super) type TotalSupply<T: Config> =
30+
StorageValue<_, u64, ValueQuery, TotalSupplyDefaultValue<T>>;
31+
32+
// Data structure that holds whether or not the pallet's init() function has been called
33+
#[pallet::storage]
34+
#[pallet::getter(fn is_init)]
35+
pub(super) type Init<T: Config> = StorageValue<_, bool, ValueQuery>;
36+
37+
/// Storage item for balances to accounts mapping.
38+
#[pallet::storage]
39+
#[pallet::getter(fn get_balance)]
40+
pub(super) type BalanceToAccount<T: Config> = StorageMap<
41+
_,
42+
Blake2_128Concat,
43+
T::AccountId,
44+
u64,
45+
ValueQuery
46+
>;
47+
48+
/// Token mint can emit two Event types.
49+
#[pallet::event]
50+
#[pallet::generate_deposit(pub(super) fn deposit_event)]
51+
pub enum Event<T: Config> {
52+
/// Token was initialized by user
53+
Initialized(T::AccountId),
54+
/// Tokens were successfully transferred between accounts. [from, to, value]
55+
Transferred(T::AccountId, T::AccountId, u64),
56+
}
57+
58+
#[pallet::hooks]
59+
impl<T: Config> Hooks<BlockNumberFor<T>> for Pallet<T> {}
60+
61+
#[pallet::error]
62+
pub enum Error<T> {
63+
/// Attempted to initialize the token after it had already been initialized.
64+
AlreadyInitialized,
65+
}
66+
67+
#[pallet::call]
68+
impl<T:Config> Pallet<T> {
69+
/// Initialize the token
70+
/// Transfers the total_supply amount to the caller
71+
/// If init() has already been called, throw AlreadyInitialized error
72+
#[pallet::weight(10_000)]
73+
pub fn init(origin: OriginFor<T>) -> DispatchResultWithPostInfo {
74+
let sender = ensure_signed(origin)?;
75+
ensure!(!Self::is_init(), <Error<T>>::AlreadyInitialized);
76+
77+
<BalanceToAccount<T>>::insert(sender, Self::total_supply());
78+
79+
Init::<T>::put(true);
80+
Ok(().into())
81+
}
82+
83+
/// Allow minting account to transfer a given balance to another account.
84+
///
85+
/// Parameters:
86+
/// - `to`: The account to receive the transfer.
87+
/// - `amount`: The amount of balance to transfer.
88+
///
89+
/// Emits `Transferred` event when successful.
90+
#[pallet::weight(10_000)]
91+
pub fn transfer(
92+
origin: OriginFor<T>,
93+
to: T::AccountId,
94+
amount: u64,
95+
) -> DispatchResultWithPostInfo {
96+
let sender = ensure_signed(origin)?;
97+
let sender_balance = Self::get_balance(&sender);
98+
let receiver_balance = Self::get_balance(&to);
99+
100+
// Calculate new balances.
101+
let update_sender = sender_balance - amount;
102+
let update_to = receiver_balance + amount;
103+
104+
// Update both accounts storage.
105+
<BalanceToAccount<T>>::insert(&sender, update_sender);
106+
<BalanceToAccount<T>>::insert(&to, update_to);
107+
108+
// Emit event.
109+
Self::deposit_event(Event::Transferred(sender, to, amount));
110+
Ok(().into())
111+
}
112+
}
113+
}
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
# Don't Panic!
2+
3+
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.
4+
5+
# Example
6+
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]`.
7+
8+
```rust
9+
/// Do some work
10+
///
11+
/// Parameters:
12+
/// - `useful_amounts`: A vector of u64 values in which there is a important value.
13+
///
14+
/// Emits `FoundVal` event when successful.
15+
#[pallet::weight(10_000)]
16+
pub fn find_important_value(
17+
origin: OriginFor<T>,
18+
useful_amounts: Vec<u64>,
19+
) -> DispatchResultWithPostInfo {
20+
let sender = ensure_signed(origin)?;
21+
22+
ensure!(useful_amounts[0] > 1_000, <Error<T>>::NoImportantValueFound);
23+
24+
// Found the important value
25+
ImportantValue::<T>::put(&useful_amounts[0]);
26+
[...]
27+
}
28+
```
29+
30+
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.
31+
32+
# Mitigations
33+
- 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.).
34+
- Proper data validation of all input parameters is crucial to ensure that an unexpected panic does not occur.
35+
- A thorough suite of unit tests should be implemented.
36+
- Fuzz testing (e.g. using [`test-fuzz`](https://github.com/trailofbits/test-fuzz)) can aid in exploring more of the input space.
37+
38+
# References
39+
- https://docs.substrate.io/main-docs/build/events-errors/#errors
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
#![cfg_attr(not(feature = "std"), no_std)]
2+
3+
pub use pallet::*;
4+
5+
#[frame_support::pallet]
6+
pub mod pallet {
7+
use frame_support::{dispatch::DispatchResultWithPostInfo, pallet_prelude::*};
8+
use frame_system::pallet_prelude::*;
9+
use sp_std::prelude::*;
10+
11+
/// Pallet configuration
12+
#[pallet::config]
13+
pub trait Config: frame_system::Config {
14+
/// Because this pallet emits events, it depends on the runtime's definition of an event.
15+
type Event: From<Event<Self>> + IsType<<Self as frame_system::Config>::Event>;
16+
}
17+
18+
#[pallet::pallet]
19+
#[pallet::without_storage_info]
20+
#[pallet::generate_store(pub(super) trait Store)]
21+
pub struct Pallet<T>(_);
22+
23+
/// Storage item for holding an ImportantValue
24+
#[pallet::storage]
25+
#[pallet::getter(fn get_val)]
26+
pub(super) type ImportantValue<T: Config> = StorageValue<_, u64, ValueQuery>;
27+
28+
#[pallet::event]
29+
#[pallet::generate_deposit(pub(super) fn deposit_event)]
30+
pub enum Event<T: Config> {
31+
/// Emit after val is found
32+
FoundVal(T::AccountId, u64),
33+
}
34+
35+
#[pallet::hooks]
36+
impl<T: Config> Hooks<BlockNumberFor<T>> for Pallet<T> {}
37+
38+
#[pallet::error]
39+
pub enum Error<T> {
40+
NoImportantValueFound,
41+
}
42+
43+
#[pallet::call]
44+
impl<T:Config> Pallet<T> {
45+
/// Find important value
46+
///
47+
/// Parameters:
48+
/// - `useful_amounts`: A vector of u64 values in which there is a important value.
49+
///
50+
/// Emits `FoundVal` event when successful.
51+
#[pallet::weight(10_000)]
52+
pub fn find_important_value(
53+
origin: OriginFor<T>,
54+
useful_amounts: Vec<u64>,
55+
) -> DispatchResultWithPostInfo {
56+
let sender = ensure_signed(origin)?;
57+
58+
ensure!(useful_amounts[0] > 1_000, <Error<T>>::NoImportantValueFound);
59+
60+
// Found the important value
61+
ImportantValue::<T>::put(&useful_amounts[0]);
62+
63+
// Emit event
64+
Self::deposit_event(Event::FoundVal(sender, useful_amounts[0]));
65+
Ok(().into())
66+
}
67+
}
68+
}
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
# Origins
2+
3+
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.
4+
5+
There are three types of origins that can used in the runtime:
6+
7+
```rust
8+
pub enum RawOrigin<AccountId> {
9+
Root,
10+
Signed(AccountId),
11+
None,
12+
}
13+
```
14+
15+
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`.
16+
17+
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`.
18+
19+
# Example
20+
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`.
21+
22+
```rust
23+
#[pallet::call]
24+
impl<T:Config> Pallet<T> {
25+
/// Set the important val
26+
/// Should be only callable by ForceOrigin
27+
#[pallet::weight(10_000)]
28+
pub fn set_important_val(
29+
origin: OriginFor<T>,
30+
new_val: u64
31+
) -> DispatchResultWithPostInfo {
32+
let sender = ensure_signed(origin)?;
33+
// Change to new value
34+
<ImportantVal<T>>::put(new_val);
35+
36+
// Emit event
37+
Self::deposit_event(Event::ImportantValSet(sender, new_val));
38+
39+
Ok(().into())
40+
}
41+
}
42+
```
43+
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:
44+
45+
```rust
46+
T::ForceOrigin::ensure_origin(origin.clone())?;
47+
let sender = ensure_signed(origin)?;
48+
```
49+
50+
# Mitigations
51+
- Ensure that the correct access controls are placed on privileged functions.
52+
- Develop user documentation on all risks associated with the system, including those associated with privileged users.
53+
- A thorough suite of unit tests that validates access controls is crucial.
54+
55+
# References
56+
- https://docs.substrate.io/main-docs/build/origins/
57+
- https://docs.substrate.io/tutorials/work-with-pallets/specify-the-origin-for-a-call/
58+
- https://paritytech.github.io/substrate/master/pallet_sudo/index.html#
59+
- https://paritytech.github.io/substrate/master/pallet_democracy/index.html

0 commit comments

Comments
 (0)