Skip to main content
Sigvex

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.

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.

References