Skip to content
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

Insufficiently random values #41

Merged
merged 9 commits into from
Dec 1, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .github/workflows/test-detectors.yml
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,7 @@ jobs:
test:
[
"divide-before-multiply",
"insufficiently-random-values",
"overflow-check",
"unprotected-update-current-contract-wasm",
"unsafe-expect",
Expand Down
20 changes: 20 additions & 0 deletions detectors/insufficiently-random-values/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
[package]
name = "insufficiently-random-values"
version = "0.1.0"
edition = "2021"

[lib]
crate-type = ["cdylib"]

[dependencies]
clippy_utils = { workspace = true }
dylint_linting = { workspace = true }
if_chain = { workspace = true }

scout-audit-internal = { workspace = true }

[dev-dependencies]
dylint_testing = { workspace = true }

[package.metadata.rust-analyzer]
rustc_private = true
45 changes: 45 additions & 0 deletions detectors/insufficiently-random-values/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
#![feature(rustc_private)]

extern crate rustc_hir;

use if_chain::if_chain;
use rustc_hir::{BinOpKind, Expr, ExprKind};
use rustc_lint::{LateContext, LateLintPass};
use scout_audit_internal::Detector;

dylint_linting::declare_late_lint! {
/// ### What it does
/// This detector prevents the usage of timestamp/sequence number and modulo operator as a random number source.
///
/// ### Why is this bad?
/// The value of the block timestamp and block sequence can be manipulated by validators, which means they're not a secure source of randomness. Therefore, they shouldn't be used for generating random numbers, especially in the context of a betting contract where the outcomes of bets could be manipulated.
///
/// ### Example
/// ```rust
/// let pseudo_random = env.ledger().timestamp() % max_val;
/// ```
///
pub INSUFFICIENTLY_RANDOM_VALUES,
Warn,
Detector::InsufficientlyRandomValues.get_lint_message()
}

impl<'tcx> LateLintPass<'tcx> for InsufficientlyRandomValues {
fn check_expr(&mut self, cx: &LateContext<'tcx>, expr: &'tcx Expr<'_>) {
if_chain! {
if let ExprKind::Binary(op, lexp, _rexp) = expr.kind;
if op.node == BinOpKind::Rem;
if let ExprKind::MethodCall(path, _, _, _) = lexp.kind;
if path.ident.as_str() == "timestamp" ||
path.ident.as_str() == "sequence";
then {
Detector::InsufficientlyRandomValues.span_lint_and_help(
cx,
INSUFFICIENTLY_RANDOM_VALUES,
expr.span,
&format!("This expression seems to use ledger().{}() as a pseudo random number",path.ident.as_str()),
);
}
}
}
}
4 changes: 3 additions & 1 deletion scout-audit-internal/src/detector.rs
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ use strum::{Display, EnumIter};
#[strum(serialize_all = "kebab-case")]
pub enum Detector {
DivideBeforeMultiply,
InsufficientlyRandomValues,
OverflowCheck,
UnprotectedUpdateCurrentContractWasm,
UnsafeExpect,
Expand All @@ -37,12 +38,13 @@ impl Detector {
/// Returns the lint message for the detector.
pub const fn get_lint_message(&self) -> &'static str {
match self {
Detector::InsufficientlyRandomValues => INSUFFICIENTLY_RANDOM_VALUES_LINT_MESSAGE,
Detector::DivideBeforeMultiply => DIVIDE_BEFORE_MULTIPLY_LINT_MESSAGE,
Detector::UnsafeExpect => UNSAFE_EXPECT_LINT_MESSAGE,
Detector::OverflowCheck => OVERFLOW_CHECK_LINT_MESSAGE,
Detector::UnprotectedUpdateCurrentContractWasm => {
UNPROTECTED_UPDATE_CURRENT_CONTRACT_MESSAGE
}
Detector::UnsafeExpect => UNSAFE_EXPECT_LINT_MESSAGE,
Detector::UnsafeUnwrap => UNSAFE_UNWRAP_MESSAGE,
}
}
Expand Down
1 change: 1 addition & 0 deletions scout-audit-internal/src/detector/lint_message.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
pub const INSUFFICIENTLY_RANDOM_VALUES_LINT_MESSAGE: &str = "Use env.prng() to generate random numbers, and remember that all random numbers are under the control of validators";
pub const DIVIDE_BEFORE_MULTIPLY_LINT_MESSAGE: &str =
"Division before multiplication might result in a loss of precision";
pub const OVERFLOW_CHECK_LINT_MESSAGE: &str = "Use `overflow-checks = true` in Cargo.toml profile";
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@

[package]
name = "insufficiently-random-values-remediated-1"
version = "0.1.0"
edition = "2021"

[lib]
crate-type = ["cdylib"]

[dependencies]
soroban-sdk = "20.0.0-rc2"

[dev_dependencies]
soroban-sdk = { version = "20.0.0-rc2", features = ["testutils"] }

[features]
testutils = ["soroban-sdk/testutils"]

[profile.release]
opt-level = "z"
overflow-checks = true
debug = 0
strip = "symbols"
debug-assertions = false
panic = "abort"
codegen-units = 1
lto = true

[profile.release-with-logs]
inherits = "release"
debug-assertions = true
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
#![no_std]

use soroban_sdk::{contract, contracterror, contractimpl, Env};

#[contracterror]
#[derive(Copy, Clone, Debug, Eq, PartialEq, PartialOrd, Ord)]
#[repr(u32)]
pub enum Error {
MaxValZero = 1,
}

#[contract]
pub struct Contract;

#[contractimpl]
impl Contract {
pub fn generate_random_value(env: Env, max_val: u64) -> Result<u64, Error> {
if max_val == 0 {
Err(Error::MaxValZero)
} else {
let val = env.prng().u64_in_range(0..max_val);
Ok(val)
}
}
}

#[cfg(test)]
mod test {
use soroban_sdk::Env;

use crate::{Contract, ContractClient};

#[test]
fn random_value_sequence() {
// Given
let env = Env::default();
let contract_id = env.register_contract(None, Contract);
let client = ContractClient::new(&env, &contract_id);

// When
let first_random_value = client.generate_random_value(&10);
let second_random_value = client.generate_random_value(&10);
let third_random_value = client.generate_random_value(&10);
let fourth_random_value = client.generate_random_value(&10);
let fifth_random_value = client.generate_random_value(&10);

// Then
assert_eq!(first_random_value, 6);
assert_eq!(second_random_value, 5);
assert_eq!(third_random_value, 8);
assert_eq!(fourth_random_value, 8);
assert_eq!(fifth_random_value, 4);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@

[package]
name = "insufficiently-random-values-vulnerable-1"
version = "0.1.0"
edition = "2021"

[lib]
crate-type = ["cdylib"]

[dependencies]
soroban-sdk = "20.0.0-rc2"

[dev_dependencies]
soroban-sdk = { version = "20.0.0-rc2", features = ["testutils"] }

[features]
testutils = ["soroban-sdk/testutils"]

[profile.release]
opt-level = "z"
overflow-checks = true
debug = 0
strip = "symbols"
debug-assertions = false
panic = "abort"
codegen-units = 1
lto = true

[profile.release-with-logs]
inherits = "release"
debug-assertions = true
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
#![no_std]

use soroban_sdk::{contract, contracterror, contractimpl, Env};

#[contracterror]
#[derive(Copy, Clone, Debug, Eq, PartialEq, PartialOrd, Ord)]
#[repr(u32)]
pub enum Error {
MaxValZero = 1,
}

#[contract]
pub struct Contract;

#[contractimpl]
impl Contract {
pub fn generate_random_value_timestamp(env: Env, max_val: u64) -> Result<u64, Error> {
if max_val == 0 {
Err(Error::MaxValZero)
} else {
let val = env.ledger().timestamp() % max_val;
Ok(val)
}
}
pub fn generate_random_value_sequence(env: Env, max_val: u32) -> Result<u32, Error> {
if max_val == 0 {
Err(Error::MaxValZero)
} else {
let val = env.ledger().sequence() % max_val;
Ok(val)
}
}
}

#[cfg(test)]
mod test {
use soroban_sdk::{
testutils::{Ledger, LedgerInfo},
Env,
};

use crate::{Contract, ContractClient};

#[test]
fn random_value_sequence() {
// Given
let env = Env::default();
let contract_id = env.register_contract(None, Contract);
let client = ContractClient::new(&env, &contract_id);

// When
let mut ledger = LedgerInfo {
sequence_number: 0,
..Default::default()
};
env.ledger().set(ledger.clone());
let first_random_value = client.generate_random_value_sequence(&10);

ledger.sequence_number = 1;
env.ledger().set(ledger.clone());
let second_random_value = client.generate_random_value_sequence(&10);

ledger.sequence_number = 11;
env.ledger().set(ledger.clone());
let third_random_value = client.generate_random_value_sequence(&10);

// Then
assert_eq!(first_random_value, 0);
assert_eq!(second_random_value, 1);
assert_eq!(third_random_value, 1);
}

#[test]
fn random_value_timestamp() {
// Given
let env = Env::default();
let contract_id = env.register_contract(None, Contract);
let client = ContractClient::new(&env, &contract_id);

// When
let mut ledger = LedgerInfo {
timestamp: 0,
..Default::default()
};
env.ledger().set(ledger.clone());
let first_random_value = client.generate_random_value_timestamp(&10);

ledger.timestamp = 1;
env.ledger().set(ledger.clone());
let second_random_value = client.generate_random_value_timestamp(&10);

ledger.timestamp = 11;
env.ledger().set(ledger.clone());
let third_random_value = client.generate_random_value_timestamp(&10);

// Then
assert_eq!(first_random_value, 0);
assert_eq!(second_random_value, 1);
assert_eq!(third_random_value, 1);
}
}