Skip to content

Commit

Permalink
Detector: Bad use of tx.origin (Cyfrin#642)
Browse files Browse the repository at this point in the history
Co-authored-by: Alex Roan <alex@cyfrin.io>
  • Loading branch information
TilakMaddy and alexroan authored Aug 2, 2024
1 parent c9c1bd1 commit 4454c90
Show file tree
Hide file tree
Showing 8 changed files with 403 additions and 13 deletions.
5 changes: 5 additions & 0 deletions aderyn_core/src/detect/detector.rs
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@ pub fn get_all_issue_detectors() -> Vec<Box<dyn IssueDetector>> {
Box::<WeakRandomnessDetector>::default(),
Box::<PreDeclaredLocalVariableUsageDetector>::default(),
Box::<DeletionNestedMappingDetector>::default(),
Box::<TxOriginUsedForAuthDetector>::default(),
Box::<MsgValueUsedInLoopDetector>::default(),
Box::<ContractLocksEtherDetector>::default(),
]
Expand Down Expand Up @@ -146,6 +147,7 @@ pub(crate) enum IssueDetectorNamePool {
WeakRandomness,
PreDeclaredLocalVariableUsage,
DeleteNestedMapping,
TxOriginUsedForAuth,
MsgValueInLoop,
ContractLocksEther,
// NOTE: `Undecided` will be the default name (for new bots).
Expand Down Expand Up @@ -306,6 +308,9 @@ pub fn request_issue_detector_by_name(detector_name: &str) -> Option<Box<dyn Iss
IssueDetectorNamePool::DeleteNestedMapping => {
Some(Box::<DeletionNestedMappingDetector>::default())
}
IssueDetectorNamePool::TxOriginUsedForAuth => {
Some(Box::<TxOriginUsedForAuthDetector>::default())
}
IssueDetectorNamePool::MsgValueInLoop => Some(Box::<MsgValueUsedInLoopDetector>::default()),
IssueDetectorNamePool::ContractLocksEther => {
Some(Box::<ContractLocksEtherDetector>::default())
Expand Down
2 changes: 2 additions & 0 deletions aderyn_core/src/detect/high/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ pub(crate) mod storage_array_edit_with_memory;
pub(crate) mod storage_signed_integer_array;
pub(crate) mod tautological_compare;
pub(crate) mod tautology_or_contradiction;
pub(crate) mod tx_origin_used_for_auth;
pub(crate) mod unchecked_return;
pub(crate) mod unchecked_send;
pub(crate) mod uninitialized_state_variable;
Expand Down Expand Up @@ -62,6 +63,7 @@ pub use storage_array_edit_with_memory::StorageArrayEditWithMemoryDetector;
pub use storage_signed_integer_array::StorageSignedIntegerArrayDetector;
pub use tautological_compare::TautologicalCompareDetector;
pub use tautology_or_contradiction::TautologyOrContraditionDetector;
pub use tx_origin_used_for_auth::TxOriginUsedForAuthDetector;
pub use unchecked_return::UncheckedReturnDetector;
pub use unchecked_send::UncheckedSendDetector;
pub use uninitialized_state_variable::UninitializedStateVariableDetector;
Expand Down
171 changes: 171 additions & 0 deletions aderyn_core/src/detect/high/tx_origin_used_for_auth.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,171 @@
use std::collections::BTreeMap;
use std::error::Error;

use crate::ast::{ASTNode, Expression, Identifier, NodeID};

use crate::capture;
use crate::context::browser::ExtractMemberAccesses;
use crate::context::investigator::{
StandardInvestigationStyle, StandardInvestigator, StandardInvestigatorVisitor,
};
use crate::detect::detector::IssueDetectorNamePool;
use crate::{
context::workspace_context::WorkspaceContext,
detect::detector::{IssueDetector, IssueSeverity},
};
use eyre::Result;

#[derive(Default)]
pub struct TxOriginUsedForAuthDetector {
// Keys are: [0] source file name, [1] line number, [2] character location of node.
// Do not add items manually, use `capture!` to add nodes to this BTreeMap.
found_instances: BTreeMap<(String, usize, String), NodeID>,
}

impl IssueDetector for TxOriginUsedForAuthDetector {
fn detect(&mut self, context: &WorkspaceContext) -> Result<bool, Box<dyn Error>> {
for if_statement in context.if_statements() {
// Check within the condition block only
let ast_node: ASTNode = if_statement.condition.clone().into();
self.check_eligibility_and_capture(context, &[&ast_node], &(if_statement.into()))?;
}

for function_call in context.function_calls() {
if let Expression::Identifier(Identifier { name, .. }) =
function_call.expression.as_ref()
{
if name != "require" {
continue;
}

// Now, check for arguments of the `require(..., "message")` function call
let arguments = function_call
.arguments
.clone()
.into_iter()
.map(|n| n.into())
.collect::<Vec<ASTNode>>();

let ast_nodes: &[&ASTNode] = &(arguments.iter().collect::<Vec<_>>());
self.check_eligibility_and_capture(context, ast_nodes, &(function_call.into()))?;
}
}

Ok(!self.found_instances.is_empty())
}

fn severity(&self) -> IssueSeverity {
IssueSeverity::High
}

fn title(&self) -> String {
String::from("Potential use of `tx.origin` for authentication.")
}

fn description(&self) -> String {
String::from("Using `tx.origin` may lead to problems when users are interacting via smart contract with your \
protocol. It is recommended to use `msg.sender` for authentication.")
}

fn instances(&self) -> BTreeMap<(String, usize, String), NodeID> {
self.found_instances.clone()
}

fn name(&self) -> String {
format!("{}", IssueDetectorNamePool::TxOriginUsedForAuth)
}
}

impl TxOriginUsedForAuthDetector {
fn check_eligibility_and_capture(
&mut self,
context: &WorkspaceContext,
check_nodes: &[&ASTNode],
capture_node: &ASTNode,
) -> Result<(), Box<dyn Error>> {
// Boilerplate
let mut tracker = MsgSenderAndTxOriginTracker::default();
let investigator = StandardInvestigator::new(
context,
check_nodes,
StandardInvestigationStyle::Downstream,
)?;
investigator.investigate(context, &mut tracker)?;

if tracker.satisifed() {
capture!(self, context, capture_node);
}
Ok(())
}
}

#[derive(Default)]
struct MsgSenderAndTxOriginTracker {
reads_msg_sender: bool,
reads_tx_origin: bool,
}

impl MsgSenderAndTxOriginTracker {
/// To avoid FP (msg.sender == tx.origin) we require that tx.origin is present and msg.sender is absent
/// for it to be considered satisfied
fn satisifed(&self) -> bool {
self.reads_tx_origin && !self.reads_msg_sender
}
}

impl StandardInvestigatorVisitor for MsgSenderAndTxOriginTracker {
fn visit_any(&mut self, node: &crate::ast::ASTNode) -> eyre::Result<()> {
let member_accesses = ExtractMemberAccesses::from(node).extracted;

let has_msg_sender = member_accesses.iter().any(|member_access| {
member_access.member_name == "sender"
&& if let Expression::Identifier(identifier) = member_access.expression.as_ref() {
identifier.name == "msg"
} else {
false
}
});
self.reads_msg_sender = self.reads_msg_sender || has_msg_sender;

let has_tx_origin = member_accesses.iter().any(|member_access| {
member_access.member_name == "origin"
&& if let Expression::Identifier(identifier) = member_access.expression.as_ref() {
identifier.name == "tx"
} else {
false
}
});
self.reads_tx_origin = self.reads_tx_origin || has_tx_origin;

Ok(())
}
}

#[cfg(test)]
mod tx_origin_used_for_auth_detector {
use serial_test::serial;

use crate::detect::{
detector::IssueDetector, high::tx_origin_used_for_auth::TxOriginUsedForAuthDetector,
};

#[test]
#[serial]
fn test_tx_origin_used_for_auth() {
let context = crate::detect::test_utils::load_solidity_source_unit(
"../tests/contract-playground/src/TxOriginUsedForAuth.sol",
);

let mut detector = TxOriginUsedForAuthDetector::default();
let found = detector.detect(&context).unwrap();
// assert that the detector found an issue
assert!(found);
// assert that the detector found the correct number of instances
assert_eq!(detector.instances().len(), 3);
// assert the severity is high
assert_eq!(
detector.severity(),
crate::detect::detector::IssueSeverity::High
);
}
}
1 change: 1 addition & 0 deletions reports/adhoc-sol-files-highs-only-report.json
Original file line number Diff line number Diff line change
Expand Up @@ -195,6 +195,7 @@
"weak-randomness",
"pre-declared-local-variable-usage",
"delete-nested-mapping",
"tx-origin-used-for-auth",
"msg-value-in-loop",
"contract-locks-ether"
]
Expand Down
48 changes: 45 additions & 3 deletions reports/report.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"files_summary": {
"total_source_units": 76,
"total_sloc": 2183
"total_source_units": 77,
"total_sloc": 2225
},
"files_details": {
"files_details": [
Expand Down Expand Up @@ -193,6 +193,10 @@
"file_path": "src/TestERC20.sol",
"n_sloc": 62
},
{
"file_path": "src/TxOriginUsedForAuth.sol",
"n_sloc": 42
},
{
"file_path": "src/UncheckedReturn.sol",
"n_sloc": 33
Expand Down Expand Up @@ -312,7 +316,7 @@
]
},
"issue_count": {
"high": 35,
"high": 36,
"low": 25
},
"high_issues": {
Expand Down Expand Up @@ -1850,6 +1854,31 @@
}
]
},
{
"title": "Potential use of `tx.origin` for authentication.",
"description": "Using `tx.origin` may lead to problems when users are interacting via smart contract with your protocol. It is recommended to use `msg.sender` for authentication.",
"detector_name": "tx-origin-used-for-auth",
"instances": [
{
"contract_path": "src/TxOriginUsedForAuth.sol",
"line_no": 40,
"src": "1117:183",
"src_char": "1117:183"
},
{
"contract_path": "src/TxOriginUsedForAuth.sol",
"line_no": 51,
"src": "1431:90",
"src_char": "1431:90"
},
{
"contract_path": "src/TxOriginUsedForAuth.sol",
"line_no": 59,
"src": "1610:68",
"src_char": "1610:68"
}
]
},
{
"title": "Loop contains `msg.value`.",
"description": "Provide an explicit array of amounts alongside the receivers array, and check that the sum of all amounts matches `msg.value`.",
Expand Down Expand Up @@ -2250,6 +2279,12 @@
"src": "32:23",
"src_char": "32:23"
},
{
"contract_path": "src/TxOriginUsedForAuth.sol",
"line_no": 2,
"src": "32:24",
"src_char": "32:24"
},
{
"contract_path": "src/UncheckedSend.sol",
"line_no": 2,
Expand Down Expand Up @@ -3128,6 +3163,12 @@
"src": "32:23",
"src_char": "32:23"
},
{
"contract_path": "src/TxOriginUsedForAuth.sol",
"line_no": 2,
"src": "32:24",
"src_char": "32:24"
},
{
"contract_path": "src/UnsafeERC721Mint.sol",
"line_no": 2,
Expand Down Expand Up @@ -4060,6 +4101,7 @@
"weak-randomness",
"pre-declared-local-variable-usage",
"delete-nested-mapping",
"tx-origin-used-for-auth",
"msg-value-in-loop",
"contract-locks-ether"
]
Expand Down
Loading

0 comments on commit 4454c90

Please sign in to comment.