Secondary Signer Validation
Detects accounts with identity validation (CheckKey) but missing signer validation (CheckSigner), allowing authorization bypass.
Secondary Signer Validation
Overview
Remediation Guide: How to Fix Missing Secondary Signer Validation
The secondary signer validation detector identifies Solana programs where an account’s identity is validated via address comparison (CheckKey) but the account’s signer status is never verified (CheckSigner). This is a common vulnerability in multi-signature and authorization scenarios: checking that an account matches an expected address does not prove that the account’s private key holder authorized the transaction. An attacker can pass the correct account address without actually signing, bypassing authorization.
Sigvex scans all basic blocks for CheckKey and CheckSigner statements, building maps of identity-validated and signer-validated accounts. Accounts that have identity checks but no corresponding signer checks generate findings. CWE mapping: CWE-287 (Improper Authentication).
Why This Is an Issue
Address validation and signer validation serve different security purposes:
- Address check (
CheckKey): Confirms the account has the expected public key. This prevents substitution attacks but does not prove authorization. - Signer check (
CheckSigner): Confirms the account’s private key holder signed the transaction. This proves active authorization.
Without both checks, an attacker can:
- Bypass multi-sig: Pass the correct co-signer address without the co-signer actually signing, executing operations that require multiple approvals.
- Impersonate authorities: Reference a known authority address without proving control over it, gaining admin-level access.
- Manipulate governance: Submit votes or proposals using a governance authority’s address without their participation.
This pattern is found in approximately 10% of Solana security audits.
How to Resolve
Native Rust
use solana_program::{account_info::AccountInfo, entrypoint::ProgramResult, pubkey::Pubkey};
const APPROVER_ADDRESS: Pubkey = /* known approver */;
pub fn approve_transfer(accounts: &[AccountInfo]) -> ProgramResult {
let approver = &accounts[0];
// Check identity
if approver.key != &APPROVER_ADDRESS {
return Err(ProgramError::InvalidArgument);
}
// Check signer status (prevents impersonation)
if !approver.is_signer {
return Err(ProgramError::MissingRequiredSignature);
}
// Safe to proceed -- approver is verified and authorized
Ok(())
}
Anchor
#[derive(Accounts)]
pub struct ApproveTransfer<'info> {
#[account(
constraint = approver.key() == APPROVER_ADDRESS @ ErrorCode::Unauthorized
)]
pub approver: Signer<'info>, // Signer type enforces is_signer
}
Examples
Vulnerable Code
use solana_program::{account_info::AccountInfo, entrypoint::ProgramResult, pubkey::Pubkey};
const ADMIN: Pubkey = /* admin address */;
pub fn emergency_withdraw(accounts: &[AccountInfo], amount: u64) -> ProgramResult {
let admin = &accounts[0];
let vault = &accounts[1];
let recipient = &accounts[2];
// Identity check only -- anyone can pass the admin address!
if admin.key != &ADMIN {
return Err(ProgramError::InvalidArgument);
}
// No signer check -- attacker impersonates admin
**vault.lamports.borrow_mut() -= amount;
**recipient.lamports.borrow_mut() += amount;
Ok(())
}
Fixed Code
use solana_program::{account_info::AccountInfo, entrypoint::ProgramResult, pubkey::Pubkey};
const ADMIN: Pubkey = /* admin address */;
pub fn emergency_withdraw(accounts: &[AccountInfo], amount: u64) -> ProgramResult {
let admin = &accounts[0];
let vault = &accounts[1];
let recipient = &accounts[2];
// Identity check
if admin.key != &ADMIN {
return Err(ProgramError::InvalidArgument);
}
// Signer check -- proves the admin authorized this transaction
if !admin.is_signer {
return Err(ProgramError::MissingRequiredSignature);
}
**vault.lamports.borrow_mut() -= amount;
**recipient.lamports.borrow_mut() += amount;
Ok(())
}
Sample Sigvex Output
{
"detector_id": "secondary-signer-validation",
"severity": "high",
"confidence": 0.80,
"description": "Account v1 has its identity validated (CheckKey) to ensure it matches an expected address, but its signer status is never checked (CheckSigner). This allows an attacker to pass the correct account address without actually controlling it.",
"location": { "function": "emergency_withdraw", "offset": 0 }
}
Detection Methodology
The detector performs a single-pass scan across all basic blocks:
- Identity tracking: Records every
CheckKeystatement, mapping account variables to their first validation location. - Signer tracking: Records every
CheckSignerstatement, building a set of signer-validated accounts. - Gap detection: Reports accounts that appear in the identity map but not in the signer set.
Context modifiers:
- Anchor programs: confidence reduced by 60% (Anchor’s
Signertype enforces signer validation) - Admin/initialization functions: confidence reduced by 40%
- Read-only/view functions: confidence reduced by 70%
Limitations
False positives:
- Accounts that are intentionally checked by address only (e.g., read-only reference accounts, program IDs, PDAs) do not need signer validation and may be flagged. PDAs cannot sign transactions directly and are validated by address derivation.
- Programs that use
remaining_accountswith address-only checks for informational purposes may be flagged.
False negatives:
- Signer validation performed through indirect means (e.g., reading
is_signerinto a variable and branching later) may not be recognized as aCheckSignerpattern. - Address checks using non-
CheckKeypatterns (e.g., manual byte comparison) are not tracked.
Related Detectors
- Missing Signer Check — detects accounts used without any signer validation
- Account Verification Chain — detects incomplete verification chains across multiple check types