Access keys as an attack surface
NEAR access key security: full-access vs function-call keys, why contract accounts with full-access keys can be rewritten, and how to audit stale keys.
Advanced5 min read3-question check
On NEAR an account is not one key — it is a list of access keys, and every key on that list can act for the account within its permissions. That list is part of your security model: for users, for your deployer account, and especially for the contract account itself.
| Capability | Full-access key | Function-call key |
|---|---|---|
| Transfer NEAR | Yes | No |
| Attach a deposit to a call | Yes | No — gas only, paid from its allowance |
| Call contract methods | Any contract | Only its receiver_id, optionally only listed method_names |
| Deploy / redeploy code | Yes | No |
| Add or delete keys, delete the account | Yes | No |
The bug#
Rust
use near_sdk::{env, near, Promise, PublicKey};
#[near]
impl Vault {
// "recovery" helper left in from development
pub fn add_recovery_key(&mut self, public_key: PublicKey) -> Promise {
// BUG: no authorization. The caller now controls vault.near
Promise::new(env::current_account_id()).add_full_access_key(public_key)
}
}The fix#
Rust
use near_sdk::{assert_one_yocto, env, near, require, Promise, PublicKey};
#[near]
impl Vault {
#[payable]
pub fn add_recovery_key(&mut self, public_key: PublicKey) -> Promise {
assert_one_yocto(); // a function-call key can't attach this deposit
require!(
env::predecessor_account_id() == self.governance, // e.g. a DAO account
"only governance"
);
Promise::new(env::current_account_id()).add_full_access_key(public_key)
}
}Shell
# list every key on the contract account (near-cli-rs)
near account list-keys vault.near network-config mainnet now
# any full-access key here can redeploy the contract.
# If the contract should be immutable, delete them all
# (a "locked" contract); if it should be upgradable,
# route upgrades through a DAO / multisig instead.Check yourself
3 questions · progress saved in this browser