Role-Based Access Control Remediation
How to fix missing role checks, privilege escalation risks, and hardcoded role permissions.
Role-Based Access Control Remediation
Overview
Related Detector: Role-Based Access Control
RBAC vulnerabilities occur when programs lack role checks before privileged operations, hardcode role definitions, or allow unrestricted role grants. The fix involves storing roles in on-chain accounts, checking the caller’s role before every privileged operation, restricting role management to admins, and providing a revocation mechanism.
Recommended Fix
Before (Vulnerable)
pub fn set_fee(accounts: &[AccountInfo], new_fee: u64) -> ProgramResult {
let config = &accounts[0];
// No role check -- anyone can change fees
let mut data = config.try_borrow_mut_data()?;
data[0..8].copy_from_slice(&new_fee.to_le_bytes());
Ok(())
}
After (Fixed)
pub fn set_fee(accounts: &[AccountInfo], new_fee: u64) -> ProgramResult {
let caller = &accounts[0];
let config = &accounts[1];
let role_account = &accounts[2];
if !caller.is_signer {
return Err(ProgramError::MissingRequiredSignature);
}
// Verify caller holds admin role
let role_data = role_account.try_borrow_data()?;
let role_authority = Pubkey::try_from(&role_data[0..32])
.map_err(|_| ProgramError::InvalidAccountData)?;
if *caller.key != role_authority || role_data[32] != 0 /* Admin */ {
return Err(ProgramError::InvalidArgument);
}
drop(role_data);
let mut data = config.try_borrow_mut_data()?;
data[0..8].copy_from_slice(&new_fee.to_le_bytes());
Ok(())
}
Alternative Mitigations
1. Anchor role-based constraints
#[derive(Accounts)]
pub struct SetFee<'info> {
pub caller: Signer<'info>,
#[account(
constraint = role.authority == caller.key() @ ErrorCode::Unauthorized,
constraint = role.role == Role::Admin @ ErrorCode::InsufficientRole
)]
pub role: Account<'info, RoleAssignment>,
#[account(mut)]
pub config: Account<'info, ProgramConfig>,
}
2. PDA-based role encoding
Encode the role into PDA seeds so role accounts are deterministically derived:
let (role_pda, _bump) = Pubkey::find_program_address(
&[b"role", caller.key.as_ref(), &[Role::Admin as u8]],
program_id,
);
// If the PDA exists, the caller has the role
if *role_account.key != role_pda {
return Err(ProgramError::InvalidArgument);
}
3. Role grant and revoke instructions
Provide explicit instructions for managing roles, restricted to admin:
pub fn grant_role(accounts: &[AccountInfo], target: Pubkey, role: u8) -> ProgramResult {
let admin = &accounts[0];
verify_admin(admin)?;
// Create or update role assignment account
Ok(())
}
pub fn revoke_role(accounts: &[AccountInfo], target: Pubkey) -> ProgramResult {
let admin = &accounts[0];
verify_admin(admin)?;
// Close or zero out role assignment account
Ok(())
}
Common Mistakes
Mistake 1: Hardcoding role definitions in bytecode
// WRONG: cannot update roles after deployment
const ADMIN: Pubkey = solana_program::pubkey!("...");
const OPERATOR: Pubkey = solana_program::pubkey!("...");
Store roles in mutable on-chain accounts so they can be updated.
Mistake 2: Allowing anyone to grant roles
Role grant instructions must verify the caller is an admin before creating new role assignments. Without this check, any user can escalate privileges.
Mistake 3: No revocation mechanism
If a key is compromised, you need the ability to revoke its role. Always implement a revoke_role instruction alongside grant_role.