Skip to main content
Sigvex

Role-Based Access Control

Detects role-based access control vulnerabilities including missing role checks, escalation risks, and hardcoded permissions.

Role-Based Access Control

Overview

Remediation Guide: How to Fix Role-Based Access Control Issues

The RBAC detector identifies vulnerabilities in role-based access control implementations on Solana. It flags privileged operations that execute without checking the caller’s role, hardcoded role permissions that cannot be updated after deployment, role grant operations without proper authorization (escalation risk), and missing role revocation mechanisms. These issues allow unauthorized users to perform restricted operations or escalate their privileges.

Why This Is an Issue

Role-based access control separates privileges into distinct roles (admin, operator, user) so that each account only has the minimum necessary permissions. Flawed RBAC leads to:

  • Missing role checks (CWE-284): Privileged operations execute without verifying the caller holds the required role. Any user can perform admin actions.
  • Hardcoded roles (CWE-798): Role definitions embedded in bytecode cannot be updated without redeploying the program. This prevents responding to compromised keys.
  • Unvalidated role grants (CWE-269): If role grants are not restricted to admins, any user can grant themselves elevated privileges.
  • Missing revocation: Without the ability to revoke roles, compromised accounts retain their privileges indefinitely.

How to Resolve

Native Solana

use solana_program::{account_info::AccountInfo, program_error::ProgramError, pubkey::Pubkey};

#[repr(u8)]
pub enum Role { Admin = 0, Operator = 1, User = 2 }

pub fn process_admin_action(accounts: &[AccountInfo]) -> ProgramResult {
    let caller = &accounts[0];
    let role_account = &accounts[1];

    if !caller.is_signer {
        return Err(ProgramError::MissingRequiredSignature);
    }

    // Load role from on-chain account
    let role_data = role_account.try_borrow_data()?;
    let stored_key = Pubkey::try_from(&role_data[0..32])
        .map_err(|_| ProgramError::InvalidAccountData)?;
    let role = role_data[32];

    // Verify caller matches the role account
    if *caller.key != stored_key {
        return Err(ProgramError::InvalidArgument);
    }

    // Verify caller has admin role
    if role != Role::Admin as u8 {
        return Err(ProgramError::InvalidArgument);
    }

    // Safe to proceed
    Ok(())
}

Anchor

#[derive(Accounts)]
pub struct AdminAction<'info> {
    pub caller: Signer<'info>,
    #[account(
        constraint = role_account.authority == caller.key() @ ErrorCode::Unauthorized,
        constraint = role_account.role == Role::Admin @ ErrorCode::InsufficientRole
    )]
    pub role_account: Account<'info, RoleAssignment>,
}

#[account]
pub struct RoleAssignment {
    pub authority: Pubkey,
    pub role: Role,
}

Examples

Vulnerable Code

pub fn update_config(accounts: &[AccountInfo], new_fee: u64) -> ProgramResult {
    let caller = &accounts[0];
    let config = &accounts[1];

    // VULNERABLE: no role check -- any account can update config
    let mut data = config.try_borrow_mut_data()?;
    data[0..8].copy_from_slice(&new_fee.to_le_bytes());
    Ok(())
}

Fixed Code

pub fn update_config(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 has operator or admin role
    let role_data = role_account.try_borrow_data()?;
    let role_key = Pubkey::try_from(&role_data[0..32])
        .map_err(|_| ProgramError::InvalidAccountData)?;
    if *caller.key != role_key || role_data[32] > 1 {
        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(())
}

Sample Sigvex Output

{
  "detector_id": "rbac",
  "severity": "high",
  "confidence": 0.78,
  "title": "Missing Role Check Before Privileged Operation",
  "description": "Privileged operation (v5) executed without checking caller's role. Unauthorized users could perform restricted operations.",
  "location": { "function": "update_config", "block": 0, "statement": 1 },
  "cwe": 284
}

Detection Methodology

The detector analyzes role-based access patterns in the function’s intermediate representation:

  1. Role check identification: Tracks CheckKey and CheckOwner statements that indicate the caller’s role or authority is being validated.
  2. Privileged operation scanning: Identifies StoreAccountData and TransferLamports as operations requiring role-based authorization.
  3. Hardcoded role detection: Flags constant assignments to role-like variables in the parameter range, indicating roles embedded in bytecode.
  4. Role grant analysis: Detects role assignment operations and checks whether the caller’s authority is verified before granting roles (prevents escalation).
  5. Revocation check: If role grants exist but no role revocation pattern is found, a missing-revocation finding is generated.
  6. Context adjustment: Confidence is reduced for Anchor programs and read-only functions.

Limitations

False positives:

  • Anchor programs where account constraints enforce role checks before the handler.
  • Programs that use PDA derivation to implicitly encode roles (the PDA seeds contain role information).

False negatives:

  • Custom role systems with non-standard data layouts that heuristics do not recognize.
  • Role validation performed through CPI to a separate access control program.

References