7. Improper Object Sharing
Overview
Accidentally exposing objects as shared via transfer::share_object enables global mutation by anyone. Once an object is shared, it cannot be unshared — this is a permanent, irreversible change to the object’s ownership model.
Risk Level
High — Shared objects are accessible to all, potentially exposing sensitive operations.
OWASP / CWE Mapping
| OWASP Top 10 | MITRE CWE |
|---|---|
| A01 (Broken Access Control) | CWE-284 (Improper Access Control), CWE-277 (Insecure Inherited Permissions) |
The Problem
Ownership Models in Sui
| Type | Created By | Who Can Use | Can Be Changed |
|---|---|---|---|
| Address-owned | transfer::transfer |
Only owner | Yes (transfer) |
| Shared | transfer::share_object |
Anyone | No (permanent) |
| Immutable | transfer::freeze_object |
Anyone (read) | No (permanent) |
Why Sharing is Dangerous
- Global access — Any transaction can reference the shared object
- No revocation — Cannot convert back to address-owned
- Mutation exposure — All
&mutentry functions become callable by anyone - Contention — Performance issues from concurrent access
Vulnerable Example
module vulnerable::wallet {
use sui::object::{Self, UID};
use sui::tx_context::{Self, TxContext};
use sui::transfer;
use sui::coin::{Self, Coin};
use sui::sui::SUI;
public struct Wallet has key {
id: UID,
funds: Coin<SUI>,
owner: address,
}
/// VULNERABLE: Wallet is shared instead of transferred!
fun init(ctx: &mut TxContext) {
let wallet = Wallet {
id: object::new(ctx),
funds: coin::zero(ctx),
owner: tx_context::sender(ctx),
};
// WRONG: This should be transfer(), not share_object()!
transfer::share_object(wallet);
}
/// Because wallet is shared, ANYONE can call this!
public entry fun withdraw(
wallet: &mut Wallet,
amount: u64,
ctx: &mut TxContext
) {
// This check is useless — attacker just passes their address
let recipient = tx_context::sender(ctx);
// Wait, the owner check is missing entirely!
let withdrawn = coin::split(&mut wallet.funds, amount, ctx);
transfer::public_transfer(withdrawn, recipient);
}
/// VULNERABLE: Even with owner check, sharing was wrong
public entry fun withdraw_checked(
wallet: &mut Wallet,
amount: u64,
ctx: &mut TxContext
) {
// Owner check exists but...
assert!(tx_context::sender(ctx) == wallet.owner, E_NOT_OWNER);
// If owner's key is compromised, wallet is drained
// With address-owned, owner could at least try to transfer first
let withdrawn = coin::split(&mut wallet.funds, amount, ctx);
transfer::public_transfer(withdrawn, wallet.owner);
}
}Attack Scenario
// Attacker finds the shared wallet object
module attack::drain_wallet {
use vulnerable::wallet;
use sui::tx_context::TxContext;
public entry fun steal(
wallet: &mut wallet::Wallet,
ctx: &mut TxContext
) {
// Because wallet is shared, attacker can reference it directly
// If withdraw() lacks owner check, funds are gone
wallet::withdraw(wallet, 1000000, ctx);
}
}Secure Example
module secure::wallet {
use sui::object::{Self, UID};
use sui::tx_context::{Self, TxContext};
use sui::transfer;
use sui::coin::{Self, Coin};
use sui::sui::SUI;
/// Wallet is address-owned — only owner can use it
public struct Wallet has key {
id: UID,
funds: Coin<SUI>,
}
/// SECURE: Transfer to user, not share
fun init(ctx: &mut TxContext) {
transfer::transfer(
Wallet {
id: object::new(ctx),
funds: coin::zero(ctx),
},
tx_context::sender(ctx)
);
}
/// Owner must possess the wallet to call this
public entry fun withdraw(
wallet: &mut Wallet,
amount: u64,
recipient: address,
ctx: &mut TxContext
) {
let withdrawn = coin::split(&mut wallet.funds, amount, ctx);
transfer::public_transfer(withdrawn, recipient);
}
/// Only owner can transfer their wallet
public entry fun transfer_wallet(
wallet: Wallet,
new_owner: address,
) {
transfer::transfer(wallet, new_owner);
}
}When Sharing IS Appropriate
module appropriate_sharing::examples {
use sui::object::{Self, UID};
use sui::tx_context::TxContext;
use sui::transfer;
/// APPROPRIATE: Global configuration that needs to be readable by all
public struct GlobalConfig has key {
id: UID,
fee_bps: u64,
paused: bool,
}
/// APPROPRIATE: Order book that multiple parties interact with
public struct OrderBook has key {
id: UID,
bids: vector<Order>,
asks: vector<Order>,
}
/// APPROPRIATE: Liquidity pool for AMM
public struct LiquidityPool has key {
id: UID,
reserve_a: Coin<A>,
reserve_b: Coin<B>,
}
/// For shared objects, use capability-based access control
public struct AdminCap has key {
id: UID,
config_id: ID,
}
public entry fun update_config(
cap: &AdminCap,
config: &mut GlobalConfig,
new_fee: u64,
) {
assert!(cap.config_id == object::id(config), E_WRONG_CONFIG);
config.fee_bps = new_fee;
}
}Sharing Decision Checklist
Ask these questions before using share_object:
-
Must multiple unrelated parties access this object?
- Yes → Consider sharing
- No → Use
transfer()for address-owned
-
Does the object contain funds or valuable assets?
- Yes → Prefer address-owned or use strict capability controls
- No → Sharing may be acceptable
-
Can operations be separated into read-only vs write?
- Yes → Consider immutable config + mutable state pattern
- No → Ensure all write paths have access control
-
Is contention expected?
- Yes → Consider sharding or owned-object patterns
- No → Sharing may be acceptable
Recommended Mitigations
1. Default to Address-Owned
// Unless you have a specific reason, use transfer()
fun init(ctx: &mut TxContext) {
transfer::transfer(MyObject { ... }, tx_context::sender(ctx));
}2. Separate Shared and Owned State
/// Shared: Global registry (read-heavy)
public struct Registry has key {
id: UID,
// Immutable or admin-only mutable state
}
/// Owned: User-specific state
public struct UserAccount has key {
id: UID,
// User's private state
}3. Use Capabilities for Shared Object Mutations
/// Any mutation of shared objects requires a capability
public entry fun modify_shared(
cap: &ModifyCap,
shared: &mut SharedObject,
...
) {
assert!(cap.shared_id == object::id(shared), E_WRONG_CAP);
// Now safe to modify
}4. Document Sharing Decisions
/// SHARED: This object is intentionally shared because:
/// 1. Multiple parties need to interact (buyers/sellers)
/// 2. All mutations require OrderCap
/// 3. Contention is managed via order sharding
public struct OrderBook has key { ... }Testing Checklist
- Review all
share_objectcalls and justify each one - Verify shared objects have proper access control on all mutations
- Test that address-owned objects cannot be accessed by non-owners
- Confirm no sensitive operations are exposed on shared objects without capability checks
- Document the ownership model for each object type