Capability leakage occurs when authority-granting objects (capabilities) are unintentionally exposed through return values, public functions, or parent struct access. Once a capability leaks, unauthorized parties can perform privileged operations.
Risk Level
Critical — Direct path to unauthorized privileged access.
OWASP / CWE Mapping
OWASP Top 10
MITRE CWE
A01 (Broken Access Control)
CWE-284 (Improper Access Control), CWE-668 (Exposure of Resource to Wrong Sphere)
The Problem
How Capabilities Leak
Returning capabilities by value — Functions that return capability objects
Parent struct exposure — Returning structs containing capability children
Public field access — Capabilities stored in accessible fields
Dynamic field exposure — Capabilities stored as retrievable dynamic fields
Vulnerable Example
modulevulnerable::protocol{usesui::object::{Self,UID};usesui::tx_context::TxContext;usesui::dynamic_fieldasdf;publicstructAdminCaphaskey,store{id: UID,}publicstructProtocolStatehaskey{id: UID,admin_cap: AdminCap,// Capability embedded in state!
}publicstructCapWrapperhaskey,store{id: UID,cap: AdminCap,}/// VULNERABLE: Returns parent containing capability
publicfunget_state(state: &mutProtocolState): ProtocolState{// Caller now has access to admin_cap!
*state}/// VULNERABLE: Exposes capability through wrapper
publicfunget_wrapper(state: &ProtocolState): &CapWrapper{// If wrapper is extractable, cap is leaked
&state.wrapper}/// VULNERABLE: Creates accessor that leaks authority
publicfunborrow_admin_cap(state: &ProtocolState): &AdminCap{// Even a reference can be used to call admin functions!
&state.admin_cap}/// VULNERABLE: Dynamic field stores capability
publicfunstore_cap_in_field(parent: &mutUID,cap: AdminCap,){df::add(parent,b"admin",cap);}/// Anyone who knows the key can retrieve it
publicfunget_cap_from_field(parent: &mutUID): &AdminCap{df::borrow(parent,b"admin")}}modulevulnerable::treasury{usevulnerable::protocol::AdminCap;/// VULNERABLE: Accepts capability reference
/// Anyone who leaked the reference can call this
publicentryfundrain_treasury(_cap: &AdminCap,treasury: &mutTreasury,ctx: &mutTxContext){// No additional checks — trusting the capability
letall_funds=treasury.balance;treasury.balance=0;// ... transfer funds
}}
Attack Scenario
moduleattack::exploit{usevulnerable::protocol;publicentryfunsteal_admin(state: &vulnerable::protocol::ProtocolState,treasury: &mutTreasury,ctx: &mutTxContext){// Leak the capability reference
letcap_ref=protocol::borrow_admin_cap(state);// Use leaked capability to drain treasury
vulnerable::treasury::drain_treasury(cap_ref,treasury,ctx);}}
Secure Example
modulesecure::protocol{usesui::object::{Self,UID,ID};usesui::tx_context::{Self,TxContext};usesui::transfer;/// Capability with no `store` — cannot be wrapped or transferred
publicstructAdminCaphaskey{id: UID,protocol_id: ID,authorized_address: address,}/// State does NOT contain capability
publicstructProtocolStatehaskey{id: UID,admin_cap_id: ID,// Only stores the ID, not the cap itself
treasury_balance: u64,}funinit(ctx: &mutTxContext){letstate=ProtocolState{id: object::new(ctx),admin_cap_id: object::id_from_address(@0x0),// Placeholder
treasury_balance: 0,};letstate_id=object::id(&state);letcap=AdminCap{id: object::new(ctx),protocol_id: state_id,authorized_address: tx_context::sender(ctx),};// Update state with cap ID
state.admin_cap_id=object::id(&cap);transfer::share_object(state);transfer::transfer(cap,tx_context::sender(ctx));}/// SECURE: No capability return — action performed internally
publicentryfunadmin_withdraw(cap: &AdminCap,state: &mutProtocolState,amount: u64,ctx: &mutTxContext){// Verify cap matches this protocol
assert!(cap.protocol_id==object::id(state),E_WRONG_PROTOCOL);// Verify caller is authorized holder
assert!(tx_context::sender(ctx)==cap.authorized_address,E_NOT_AUTHORIZED);// Perform action directly — no capability exposure
assert!(state.treasury_balance>=amount,E_INSUFFICIENT);state.treasury_balance=state.treasury_balance-amount;// ... transfer funds
}/// SECURE: View function returns data, not capability
publicfunget_admin_cap_id(state: &ProtocolState): ID{state.admin_cap_id}/// SECURE: Check authorization without exposing capability
publicfunis_admin(cap: &AdminCap,state: &ProtocolState): bool{cap.protocol_id==object::id(state)}}
Capability Protection Patterns
Pattern 1: Action Functions Instead of Capability Exposure
/// Capabilities stored separately, not in protocol state
publicstructCapabilityRegistryhaskey{id: UID,// Only IDs, not actual capabilities
admin_cap_ids: vector<ID>,}/// Capabilities owned by users, not stored centrally
publicstructAdminCaphaskey{id: UID,registry_id: ID,}
Pattern 3: Witness Pattern for One-Time Auth
/// Witness can only be created once (in init)
publicstructPROTOCOLhasdrop{}/// Auth checked via witness possession
publicfunauthorized_action<T: drop>(_witness: T,state: &mutState,){// Only code with the witness type can call
}
Pattern 4: Hot Potato for Scoped Authority
/// Hot potato — must be consumed in same transaction
publicstructAdminSession{state_id: ID,action_count: u64,max_actions: u64,}publicfunstart_admin_session(cap: &AdminCap,state: &State,): AdminSession{verify_cap(cap,state);AdminSession{state_id: object::id(state),action_count: 0,max_actions: 10,}}publicfunadmin_action(session: &mutAdminSession,state: &mutState,){assert!(session.state_id==object::id(state),E_WRONG_STATE);assert!(session.action_count<session.max_actions,E_MAX_ACTIONS);session.action_count=session.action_count+1;// Perform action
}publicfunend_admin_session(session: AdminSession){letAdminSession{state_id: _,action_count: _,max_actions: _}=session;// Session consumed
}
Recommended Mitigations
1. Never Return Capabilities
// BAD
publicfunget_cap(): AdminCap{...}publicfunborrow_cap(): &AdminCap{...}// GOOD
publicentryfunuse_cap_for_action(cap: &AdminCap,...){...}
2. Remove store from Capabilities
/// Without `store`, cap cannot be wrapped or dynamically stored
publicstructAdminCaphaskey{id: UID,}
3. Tie Capabilities to Specific Resources
publicstructVaultAdminCaphaskey{id: UID,vault_id: ID,// Only valid for this specific vault
}
4. Use Capability References, Not Values
/// Functions should borrow capabilities, not consume them
publicentryfunaction(cap: &AdminCap,...){}// Borrow
/// Only transfer functions should consume
publicentryfuntransfer_admin(cap: AdminCap,new_admin: address){}
Testing Checklist
Verify no functions return capability objects
Confirm no functions return structs containing capabilities
Check that capabilities lack store ability
Verify capabilities are not stored in dynamic fields accessibly
Test that leaked references cannot bypass access control
Audit all places where capability references are passed