Skip to main content
Sigvex

Signer Authority Role

Detects privileged operations with signer checks but no authority role validation, allowing any signer to execute admin actions.

Signer Authority Role

Overview

Remediation Guide: How to Fix Signer Authority Role Issues

The signer authority role detector identifies Solana programs where privileged operations verify that an account is a signer but do not validate that the signer holds a specific authority role (admin, owner, operator). This means any wallet that signs the transaction can perform operations intended only for authorized parties – including fund transfers, configuration changes, and program upgrades via CPI.

Why This Is an Issue

Checking is_signer confirms the account signed the transaction but says nothing about who the signer is. Without authority validation:

  • Any signer can transfer funds (CWE-284): Lamport transfers protected only by is_signer allow any wallet to drain the treasury.
  • Any signer can modify state: Configuration data, fee parameters, and account state can be tampered with by unauthorized users.
  • Privilege escalation via CPI (CWE-269): Privileged CPI calls (program upgrades, admin operations on other programs) can be triggered by any signer, potentially compromising the program or downstream programs.

This is distinct from a missing signer check – the signer check exists but is insufficient. The program has the appearance of security without the substance.

How to Resolve

Native Solana

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

pub fn admin_transfer(
    accounts: &[AccountInfo],
    amount: u64,
) -> Result<(), ProgramError> {
    let authority = &accounts[0];
    let vault = &accounts[1];
    let config = &accounts[2];

    // Signer check (necessary but not sufficient)
    if !authority.is_signer {
        return Err(ProgramError::MissingRequiredSignature);
    }

    // Authority role validation (the critical check)
    let config_data = config.try_borrow_data()?;
    let admin_key = Pubkey::try_from(&config_data[0..32])
        .map_err(|_| ProgramError::InvalidAccountData)?;
    if *authority.key != admin_key {
        return Err(ProgramError::InvalidArgument);
    }

    // Now safe -- caller is both signer AND admin
    **vault.try_borrow_mut_lamports()? -= amount;
    **authority.try_borrow_mut_lamports()? += amount;
    Ok(())
}

Anchor

#[derive(Accounts)]
pub struct AdminTransfer<'info> {
    // Signer<'info> validates is_signer
    // has_one validates the key matches config.admin
    pub admin: Signer<'info>,
    #[account(has_one = admin @ ErrorCode::Unauthorized)]
    pub config: Account<'info, ProgramConfig>,
    #[account(mut)]
    pub vault: Account<'info, Vault>,
}

#[account]
pub struct ProgramConfig {
    pub admin: Pubkey,
}

Examples

Vulnerable Code

pub fn withdraw(accounts: &[AccountInfo], amount: u64) -> ProgramResult {
    let signer = &accounts[0];
    let vault = &accounts[1];

    // Checks signer -- but ANY signer can call
    if !signer.is_signer {
        return Err(ProgramError::MissingRequiredSignature);
    }

    // VULNERABLE: no authority check
    **vault.try_borrow_mut_lamports()? -= amount;
    **signer.try_borrow_mut_lamports()? += amount;
    Ok(())
}

Fixed Code

pub fn withdraw(accounts: &[AccountInfo], amount: u64) -> ProgramResult {
    let authority = &accounts[0];
    let vault = &accounts[1];
    let config = &accounts[2];

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

    // Validate authority role
    let config_data = config.try_borrow_data()?;
    let expected = Pubkey::try_from(&config_data[0..32])
        .map_err(|_| ProgramError::InvalidAccountData)?;
    if *authority.key != expected {
        return Err(ProgramError::InvalidArgument);
    }

    drop(config_data);
    **vault.try_borrow_mut_lamports()? -= amount;
    **authority.try_borrow_mut_lamports()? += amount;
    Ok(())
}

Sample Sigvex Output

{
  "detector_id": "signer-authority-role",
  "severity": "high",
  "confidence": 0.75,
  "title": "Lamport Transfer With Signer But No Authority Validation",
  "description": "Lamport transfer requires signer but doesn't validate that the signer has authority (e.g., admin, owner). Any signer can transfer lamports, which could lead to unauthorized fund movements.",
  "location": { "function": "withdraw", "block": 0, "statement": 1 },
  "cwe": 284
}

Detection Methodology

The detector performs two-pass analysis over the function’s intermediate representation:

  1. Signer tracking: Records all accounts that have CheckSigner statements, building a set of confirmed signers.
  2. Authority tracking: Records accounts validated by CheckKey with a constant (non-dynamic) expected value, indicating the signer’s key is compared against a known authority address. Dynamic expected values (loaded from variables) do not count as proper authority validation.
  3. Privileged operation matching: For each TransferLamports, StoreAccountData, or InvokeCpi involving a signer-checked account, verifies that the same account also has authority validation.
  4. Severity grading: Lamport transfers and data modifications with signer-only produce high severity. Privileged CPI calls with signer-only produce critical severity (due to potential program upgrade or escalation).
  5. Context adjustment: Confidence is reduced for Anchor programs (where Signer<'info> handles signer checks but has_one/constraint must handle authority), admin functions, and read-only functions.

Limitations

False positives:

  • User-initiated operations where any signer is legitimately authorized (e.g., a user withdrawing from their own account using a PDA they own).
  • Anchor programs where has_one constraints validate authority before the handler.

False negatives:

  • Authority validation performed via CPI to an external access control program.
  • Complex authority derivation through PDA seeds or multi-step lookups.

References