Loading...
Loading...
IC-specific security patterns for canister development in Motoko and Rust. Covers access control, anonymous principal rejection, reentrancy prevention (CallerGuard pattern), async safety (saga pattern), callback trap handling, cycle drain protection, and safe upgrade patterns. Use when writing or modifying any canister that modifies state, handles tokens, makes inter-canister calls, or implements access control.
npx skill4agent add dfinity/icskills canister-securityawaitcanister_inspect_messagemopscore = "2.0.0"ic-cdk = "0.19"candid = "0.10"canister_inspect_messageinspect_message2vxsx-faePrincipal.isAnonymous(caller)msg_caller() != Principal::anonymous()awaitawaitpre_upgradepre_upgradeic-stable-structurespersistent actorletvarfreezing_threshold# Check current settings (mainnet)
icp canister settings show backend -e ic
# Increase freezing threshold for high-value canisters
icp canister settings update backend --freezing-threshold 7776000 -e ic # 90 daysicp canister settings update backend --add-controller <backup-principal> -e icfetchRootKey()fetchRootKey()fetchRootKey()fetchRootKey()try/finallyDropcall_on_cleanupawaitawaitfinallyDropshared(msg)init()import Principal "mo:core/Principal";
import Set "mo:core/pure/Set";
import Runtime "mo:core/Runtime";
shared(msg) persistent actor class MyCanister() {
// --- Authorization state ---
// transient: recomputed on each install/upgrade from msg.caller (the controller)
transient let owner = msg.caller;
var admins : Set.Set<Principal> = Set.empty();
// --- Guards ---
func requireAuthenticated(caller : Principal) {
if (Principal.isAnonymous(caller)) {
Runtime.trap("anonymous caller not allowed");
};
};
func requireOwner(caller : Principal) {
requireAuthenticated(caller);
if (caller != owner) {
Runtime.trap("caller is not the owner");
};
};
func requireAdmin(caller : Principal) {
requireAuthenticated(caller);
if (caller != owner and not Set.contains(admins, Principal.compare, caller)) {
Runtime.trap("caller is not an admin");
};
};
// --- Admin management ---
public shared ({ caller }) func addAdmin(newAdmin : Principal) : async () {
requireOwner(caller);
admins := Set.add(admins, Principal.compare, newAdmin);
};
public shared ({ caller }) func removeAdmin(admin : Principal) : async () {
requireOwner(caller);
admins := Set.remove(admins, Principal.compare, admin);
};
// --- Endpoints ---
public shared ({ caller }) func publicAction() : async Text {
requireAuthenticated(caller);
"ok";
};
public shared ({ caller }) func adminAction() : async () {
requireAdmin(caller);
// ... protected logic
};
};finallycatchfinallyimport Map "mo:core/Map";
import Principal "mo:core/Principal";
import Error "mo:core/Error";
import Result "mo:core/Result";
// Inside the persistent actor class { ... }
// otherCanister is application-specific — replace with your canister reference.
let pendingRequests = Map.empty<Principal, Bool>();
func acquireGuard(principal : Principal) : Result.Result<(), Text> {
if (Map.get(pendingRequests, Principal.compare, principal) != null) {
return #err("already processing a request for this caller");
};
Map.add(pendingRequests, Principal.compare, principal, true);
#ok;
};
func releaseGuard(principal : Principal) {
ignore Map.delete(pendingRequests, Principal.compare, principal);
};
public shared ({ caller }) func doSomethingAsync() : async Result.Result<Text, Text> {
requireAuthenticated(caller);
// 1. Acquire per-caller lock — rejects concurrent calls from same principal
switch (acquireGuard(caller)) {
case (#err(msg)) { return #err(msg) };
case (#ok) {};
};
// 2. Make inter-canister call
try {
let result = await otherCanister.someMethod();
#ok(result)
} catch (e) {
#err("call failed: " # Error.message(e))
} finally {
// Runs in cleanup context even if the callback traps — changes here persist.
releaseGuard(caller);
};
};// Inside persistent actor { ... }
// Method variants must match your public methods
system func inspect(
{
caller : Principal;
msg : {
#adminAction : () -> ();
#addAdmin : () -> Principal;
#removeAdmin : () -> Principal;
#publicAction : () -> ();
#doSomethingAsync : () -> ();
}
}
) : Bool {
switch (msg) {
// Admin methods: reject anonymous to save cycles on Candid decoding
case (#adminAction _) { not Principal.isAnonymous(caller) };
case (#addAdmin _) { not Principal.isAnonymous(caller) };
case (#removeAdmin _) { not Principal.isAnonymous(caller) };
case (#doSomethingAsync _) { not Principal.isAnonymous(caller) };
// Public methods: accept all
case (_) { true };
};
};guardErruse ic_cdk::{init, update};
use ic_cdk::api::msg_caller;
use candid::Principal;
use std::cell::RefCell;
thread_local! {
static OWNER: RefCell<Principal> = RefCell::new(Principal::anonymous());
static ADMINS: RefCell<Vec<Principal>> = RefCell::new(vec![]);
}
// --- Guards (for #[update(guard = "...")] attribute) ---
// Must return Result<(), String>. Err rejects the call.
fn require_authenticated() -> Result<(), String> {
if msg_caller() == Principal::anonymous() {
return Err("anonymous caller not allowed".to_string());
}
Ok(())
}
fn require_owner() -> Result<(), String> {
require_authenticated()?;
OWNER.with(|o| {
if msg_caller() != *o.borrow() {
return Err("caller is not the owner".to_string());
}
Ok(())
})
}
fn require_admin() -> Result<(), String> {
require_authenticated()?;
let caller = msg_caller();
let is_authorized = OWNER.with(|o| caller == *o.borrow())
|| ADMINS.with(|a| a.borrow().contains(&caller));
if !is_authorized {
return Err("caller is not an admin".to_string());
}
Ok(())
}
// --- Init ---
#[init]
fn init(owner: Principal) {
OWNER.with(|o| *o.borrow_mut() = owner);
}
// --- Endpoints ---
#[update(guard = "require_authenticated")]
fn public_action() -> String {
"ok".to_string()
}
#[update(guard = "require_admin")]
fn admin_action() {
// ... protected logic — guard already validated caller
}
#[update(guard = "require_owner")]
fn add_admin(new_admin: Principal) {
ADMINS.with(|a| a.borrow_mut().push(new_admin));
}
#[update(guard = "require_owner")]
fn remove_admin(admin: Principal) {
ADMINS.with(|a| a.borrow_mut().retain(|p| p != &admin));
}
ic_cdk::export_candid!();CallerGuardDroplet _ = CallerGuard::new(caller)?use std::cell::RefCell;
use std::collections::BTreeSet;
use candid::Principal;
use ic_cdk::update;
use ic_cdk::api::msg_caller;
use ic_cdk::call::Call;
// other_canister_id is application-specific — replace with your canister reference.
thread_local! {
static PENDING: RefCell<BTreeSet<Principal>> = RefCell::new(BTreeSet::new());
}
struct CallerGuard {
principal: Principal,
}
impl CallerGuard {
fn new(principal: Principal) -> Result<Self, String> {
PENDING.with(|p| {
if !p.borrow_mut().insert(principal) {
return Err("already processing a request for this caller".to_string());
}
Ok(Self { principal })
})
}
}
impl Drop for CallerGuard {
fn drop(&mut self) {
PENDING.with(|p| {
p.borrow_mut().remove(&self.principal);
});
}
}
#[update]
async fn do_something_async() -> Result<String, String> {
let caller = msg_caller();
if caller == Principal::anonymous() {
return Err("anonymous caller not allowed".to_string());
}
// Acquire per-caller lock — rejects concurrent calls from same principal.
// Drop releases lock even if callback traps.
let _guard = CallerGuard::new(caller)?;
// Make inter-canister call
let response = Call::bounded_wait(other_canister_id(), "some_method")
.await
.map_err(|e| format!("call failed: {:?}", e))?;
let result: String = response.candid()
.map_err(|e| format!("decode failed: {:?}", e))?;
Ok(result)
// _guard dropped here → lock released
}use ic_cdk::api::{accept_message, msg_caller, msg_method_name};
use candid::Principal;
/// Pre-filter to reduce cycle waste from spam.
/// Runs on ONE node. Can be bypassed. NOT a security check.
/// Always duplicate real access control inside each method or via guard attribute.
#[ic_cdk::inspect_message]
fn inspect_message() {
let method = msg_method_name();
match method.as_str() {
// Admin methods: only accept from non-anonymous callers
"admin_action" | "add_admin" | "remove_admin" | "do_something_async" => {
if msg_caller() != Principal::anonymous() {
accept_message();
}
// Silently reject anonymous — saves cycles on Candid decoding
}
// Public methods: accept all
_ => accept_message(),
}
}