Skip to content

Commit f3394ca

Browse files
fix: harden off-chain ABI decoding against malicious receiver metadata (Report #71024)
- Revert on-chain receiver_registry changes to keep fix entirely off-chain - Convert decodeParam panics to errors with checked type assertions - Add explicit TypeParameter rejection in ABI parameter decoding - Add defer/recover defense-in-depth in BuildOffRampExecutePTB - Fix unchecked assertions in token pool and receiver PTB construction - Add comprehensive unit tests for malformed and adversarial ABI shapes
1 parent 0d6856b commit f3394ca

File tree

11 files changed

+611
-114
lines changed

11 files changed

+611
-114
lines changed

bindings/generated/ccip/ccip/receiver_registry/receiver_registry.go

Lines changed: 17 additions & 30 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

contracts/ccip/ccip/sources/offramp_state_helper.move

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -233,7 +233,7 @@ public fun consume_any2sui_message<TypeProof: drop>(
233233
let receiver_package_id = address::from_ascii_bytes(&ascii::into_bytes(address_str));
234234

235235
let receiver_config = receiver_registry::get_receiver_config(ref, receiver_package_id);
236-
let (_, proof_typename, _) = receiver_registry::get_receiver_config_fields(receiver_config);
236+
let (_, proof_typename) = receiver_registry::get_receiver_config_fields(receiver_config);
237237
assert!(proof_typename == proof_tn.into_string(), ETypeProofMismatch);
238238

239239
client::consume_any2sui_message(message, receiver_package_id)

contracts/ccip/ccip/sources/receiver_registry.move

Lines changed: 5 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -13,12 +13,6 @@ use sui::linked_table::{Self, LinkedTable};
1313
public struct ReceiverConfig has copy, drop, store {
1414
module_name: String,
1515
proof_typename: ascii::String,
16-
/// The number of extra object IDs that the receiver's ccip_receive callback
17-
/// expects beyond the standard 3 parameters (expected_message_id,
18-
/// &CCIPObjectRef, Any2SuiMessage). The relayer uses this to validate that
19-
/// the receiverObjectIds count in a CCIP message matches what the receiver
20-
/// registered, preventing object injection attacks.
21-
expected_receiver_object_id_count: u64,
2216
}
2317

2418
public struct ReceiverRegistry has key, store {
@@ -31,7 +25,6 @@ public struct ReceiverRegistered has copy, drop {
3125
receiver_package_id: address,
3226
receiver_module_name: String,
3327
proof_typename: ascii::String,
34-
expected_receiver_object_id_count: u64,
3528
}
3629

3730
public struct ReceiverUnregistered has copy, drop {
@@ -64,7 +57,6 @@ public fun register_receiver<ProofType: drop>(
6457
ref: &mut CCIPObjectRef,
6558
publisher_wrapper: PublisherWrapper<ProofType>,
6659
_proof: ProofType,
67-
expected_receiver_object_id_count: u64,
6860
) {
6961
verify_function_allowed(
7062
ref,
@@ -81,15 +73,13 @@ public fun register_receiver<ProofType: drop>(
8173
let receiver_config = ReceiverConfig {
8274
module_name: receiver_module_name,
8375
proof_typename: proof_typename.into_string(),
84-
expected_receiver_object_id_count,
8576
};
8677
registry.receiver_configs.push_back(receiver_package_id, receiver_config);
8778

8879
event::emit(ReceiverRegistered {
8980
receiver_package_id,
9081
receiver_module_name,
9182
proof_typename: proof_typename.into_string(),
92-
expected_receiver_object_id_count,
9383
});
9484
}
9585

@@ -142,15 +132,15 @@ public fun get_receiver_config(ref: &CCIPObjectRef, receiver_package_id: address
142132
*registry.receiver_configs.borrow(receiver_package_id)
143133
}
144134

145-
public fun get_receiver_config_fields(rc: ReceiverConfig): (String, ascii::String, u64) {
146-
(rc.module_name, rc.proof_typename, rc.expected_receiver_object_id_count)
135+
public fun get_receiver_config_fields(rc: ReceiverConfig): (String, ascii::String) {
136+
(rc.module_name, rc.proof_typename)
147137
}
148138

149139
// this will return empty string if the receiver is not registered.
150140
public fun get_receiver_info(
151141
ref: &CCIPObjectRef,
152142
receiver_package_id: address,
153-
): (String, ascii::String, u64) {
143+
): (String, ascii::String) {
154144
verify_function_allowed(
155145
ref,
156146
string::utf8(b"receiver_registry"),
@@ -161,12 +151,8 @@ public fun get_receiver_info(
161151

162152
if (registry.receiver_configs.contains(receiver_package_id)) {
163153
let receiver_config = registry.receiver_configs.borrow(receiver_package_id);
164-
return (
165-
receiver_config.module_name,
166-
receiver_config.proof_typename,
167-
receiver_config.expected_receiver_object_id_count,
168-
)
154+
return (receiver_config.module_name, receiver_config.proof_typename)
169155
};
170156

171-
(string::utf8(b""), ascii::string(b""), 0)
157+
(string::utf8(b""), ascii::string(b""))
172158
}

contracts/ccip/ccip/tests/offramp_state_helper_tests.move

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -258,7 +258,6 @@ public fun test_extract_any2sui_message() {
258258
&mut ref,
259259
publisher_wrapper,
260260
TestTypeProof {},
261-
0,
262261
);
263262
package::burn_publisher(publisher);
264263

0 commit comments

Comments
 (0)