Insufficient Granularity of Access Control

Description

Insufficient Granularity of Access Control occurs when the product's protection mechanism does not account for granularity at the bit level, which allows attackers to bypass the protection mechanism. Various protection schemes such as locks, permissions, and access control lists are implemented in System-on-Chip (SoC) hardware and firmware to control access to registers and other hardware storage elements. These mechanisms may be designed with insufficient granularity—for example, protecting only a register instead of individual bits within it—allowing security bypass.

Risk

Insufficient access control granularity has severe security implications. Attackers may modify sensitive bits within otherwise accessible registers. Security configuration bits may be changed alongside non-sensitive data. Protection mechanisms may be bypassed through partial register access. Privilege escalation may occur through bit-level manipulation. Security policies cannot be properly enforced. Hardware security boundaries may be weakened. Firmware may inadvertently expose sensitive controls. Debug or test bits may be manipulated.

Solution

Implement bit-level access control where security-sensitive bits are present. Separate sensitive bits into dedicated protected registers. Use hardware masks to prevent writes to protected bits. Implement per-bit permission checking in access control logic. Document which bits require protection and why. Verify access control granularity during security reviews. Test that protected bits cannot be modified through any access path. Consider grouping related security bits together. Use write-once locks for critical configuration bits.

Common Consequences

ImpactDetails
Access ControlScope: Access Control

Modify Memory - Attackers can modify protected memory at the bit level when access control is not sufficiently granular. High likelihood when sensitive and non-sensitive bits share registers.
IntegrityScope: Integrity

Unauthorized Modification - Security configurations may be altered when bit-level protection is missing.

Example Code

Vulnerable Code

// Vulnerable: Register protection without bit-level granularity

module vulnerable_register_access (
    input wire clk,
    input wire reset_n,
    input wire [31:0] write_data,
    input wire write_enable,
    input wire [3:0] register_select,
    input wire privileged_access,
    output reg [31:0] read_data
);

    // Control register layout:
    // [0]     - Feature enable (user accessible)
    // [1]     - Debug mode (should be protected!)
    // [7:2]   - Configuration (user accessible)
    // [8]     - Security bypass (should be protected!)
    // [15:9]  - More config (user accessible)
    // [31:16] - Reserved

    reg [31:0] control_register;
    reg register_locked;

    always @(posedge clk or negedge reset_n) begin
        if (!reset_n) begin
            control_register <= 32'h0;
            register_locked <= 1'b0;
        end
        else if (write_enable && register_select == 4'b0001) begin
            // VULNERABLE: All-or-nothing protection
            // Either the entire register is writable or not
            if (!register_locked) begin
                // User can write ALL bits including security-critical ones
                control_register <= write_data;
            end
            // Once locked, NO bits can be written - too restrictive
        end
    end

    // No bit-level access control
    // Sensitive bits mixed with user-accessible bits

endmodule

// Vulnerable: Shared register with mixed sensitivity levels
module vulnerable_mixed_register (
    input wire clk,
    input wire [31:0] write_data,
    input wire write_enable,
    input wire user_access,
    output reg [31:0] config_register
);

    // Mixed register:
    // [3:0]   - User configuration
    // [4]     - Privilege escalation bit (CRITICAL!)
    // [7:5]   - User preferences
    // [8]     - Disable security checks (CRITICAL!)
    // [31:9]  - User data

    always @(posedge clk) begin
        if (write_enable) begin
            // VULNERABLE: No distinction between user and critical bits
            if (user_access) begin
                // User can write everything including critical security bits!
                config_register <= write_data;
            end
        end
    end

endmodule
// Vulnerable: Firmware with coarse access control

#define CONFIG_REG_ADDR 0x40001000

// Register bits (mixed sensitivity)
#define CFG_FEATURE_ENABLE   (1 << 0)   // User accessible
#define CFG_DEBUG_MODE       (1 << 1)   // Should be protected!
#define CFG_USER_SETTING     (0x3F << 2) // User accessible
#define CFG_SECURITY_BYPASS  (1 << 8)   // Should be protected!

// Vulnerable: All-or-nothing lock
bool register_locked = false;

void write_config_register(uint32_t value) {
    // VULNERABLE: Entire register locked or unlocked
    // No bit-level granularity
    if (!register_locked) {
        *(volatile uint32_t*)CONFIG_REG_ADDR = value;
    }
}

// User can set security-critical bits!
void user_configure(uint32_t user_settings) {
    // User provides full 32-bit value
    // Nothing prevents setting CFG_SECURITY_BYPASS or CFG_DEBUG_MODE
    write_config_register(user_settings);
}

Fixed Code

// Fixed: Bit-level access control granularity

module secure_register_access (
    input wire clk,
    input wire reset_n,
    input wire [31:0] write_data,
    input wire write_enable,
    input wire [3:0] register_select,
    input wire privileged_access,
    output reg [31:0] read_data
);

    // Control register with bit-level protection:
    // [0]     - Feature enable (user accessible)
    // [1]     - Debug mode (privileged only)
    // [7:2]   - Configuration (user accessible)
    // [8]     - Security bypass (privileged only)
    // [15:9]  - More config (user accessible)
    // [31:16] - Reserved (read as zero)

    reg [31:0] control_register;

    // Bit masks for access control
    localparam USER_WRITABLE_MASK = 32'h0000FEFD;  // Bits user can write
    localparam PRIV_WRITABLE_MASK = 32'h0000FFFF;  // Bits privileged can write
    localparam PROTECTED_BITS     = 32'h00000102;  // Debug mode + security bypass

    always @(posedge clk or negedge reset_n) begin
        if (!reset_n) begin
            control_register <= 32'h0;
        end
        else if (write_enable && register_select == 4'b0001) begin
            if (privileged_access) begin
                // Privileged: can write all implemented bits
                control_register <= (control_register & ~PRIV_WRITABLE_MASK) |
                                   (write_data & PRIV_WRITABLE_MASK);
            end else begin
                // User: can only write non-protected bits
                control_register <= (control_register & ~USER_WRITABLE_MASK) |
                                   (write_data & USER_WRITABLE_MASK);
                // Protected bits [1] and [8] are NOT modified
            end
        end
    end

    // Read returns actual value (or could mask protected bits for user)
    always @(*) begin
        if (privileged_access) begin
            read_data = control_register;
        end else begin
            // Optionally hide protected bits from unprivileged reads
            read_data = control_register & USER_WRITABLE_MASK;
        end
    end

endmodule

// Fixed: Separate registers for different protection levels
module secure_separated_registers (
    input wire clk,
    input wire reset_n,
    input wire [31:0] write_data,
    input wire write_enable,
    input wire [3:0] register_select,
    input wire privileged_access,
    output reg [31:0] read_data
);

    // Separate registers by sensitivity
    reg [31:0] user_config_register;      // Address 0x00 - user writable
    reg [31:0] security_config_register;  // Address 0x04 - privileged only
    reg [31:0] debug_config_register;     // Address 0x08 - privileged + debug auth

    always @(posedge clk or negedge reset_n) begin
        if (!reset_n) begin
            user_config_register <= 32'h0;
            security_config_register <= 32'h0;
            debug_config_register <= 32'h0;
        end
        else if (write_enable) begin
            case (register_select)
                4'b0000: begin
                    // User config - always writable
                    user_config_register <= write_data;
                end
                4'b0001: begin
                    // Security config - privileged only
                    if (privileged_access) begin
                        security_config_register <= write_data;
                    end
                    // Silently ignore unprivileged writes
                end
                4'b0010: begin
                    // Debug config - requires elevated privileges
                    if (privileged_access && debug_authenticated) begin
                        debug_config_register <= write_data;
                    end
                end
            endcase
        end
    end

endmodule
// Fixed: Firmware with bit-level access control

#define CONFIG_REG_ADDR 0x40001000

// User-accessible bits
#define CFG_FEATURE_ENABLE   (1 << 0)
#define CFG_USER_SETTING     (0x3F << 2)
#define CFG_USER_DATA        (0x7FFFFF << 9)

// Protected bits (privileged access only)
#define CFG_DEBUG_MODE       (1 << 1)
#define CFG_SECURITY_BYPASS  (1 << 8)

// Access masks
#define USER_WRITABLE_MASK   (CFG_FEATURE_ENABLE | CFG_USER_SETTING | CFG_USER_DATA)
#define PROTECTED_MASK       (CFG_DEBUG_MODE | CFG_SECURITY_BYPASS)

void write_config_register_user(uint32_t value) {
    // Read current value
    uint32_t current = *(volatile uint32_t*)CONFIG_REG_ADDR;

    // Preserve protected bits, update only user-writable bits
    uint32_t new_value = (current & ~USER_WRITABLE_MASK) |
                         (value & USER_WRITABLE_MASK);

    *(volatile uint32_t*)CONFIG_REG_ADDR = new_value;
}

void write_config_register_privileged(uint32_t value, bool is_privileged) {
    if (!is_privileged) {
        // Unprivileged: use user path
        write_config_register_user(value);
        return;
    }

    // Privileged: can write all bits
    *(volatile uint32_t*)CONFIG_REG_ADDR = value;
}

// Safe user configuration function
void user_configure(uint32_t user_settings) {
    // Automatically masks out protected bits
    write_config_register_user(user_settings);

    // Even if user tries to set SECURITY_BYPASS, it won't work
    // The bit will be masked out before writing
}

// Validation function
bool validate_access_control_granularity(void) {
    uint32_t original = *(volatile uint32_t*)CONFIG_REG_ADDR;

    // Try to set protected bits via user interface
    write_config_register_user(0xFFFFFFFF);

    uint32_t after_user_write = *(volatile uint32_t*)CONFIG_REG_ADDR;

    // Verify protected bits unchanged
    if ((after_user_write & PROTECTED_MASK) != (original & PROTECTED_MASK)) {
        log_error("Access control granularity failure!");
        return false;
    }

    // Restore original
    *(volatile uint32_t*)CONFIG_REG_ADDR = original;
    return true;
}

CVE Examples

Access control granularity issues have been found in various hardware designs where security-critical bits share registers with user-accessible configuration bits.


  • CWE-284: Improper Access Control (parent)
  • CWE-1198: Privilege Separation and Access Control Issues (category member)
  • CWE-1222: Insufficient Granularity of Address Regions (related)

References

  1. MITRE Corporation. "CWE-1220: Insufficient Granularity of Access Control." https://cwe.mitre.org/data/definitions/1220.html
  2. Hardware Security Best Practices
  3. SoC Access Control Design Guidelines