Expected Behavior Violation

Description

Expected Behavior Violation is a vulnerability where a feature, API, or function does not perform according to its specification or documented behavior. Software developers rely on documented behavior to build secure and reliable systems. When a component behaves differently than specified, callers may make incorrect assumptions that lead to security vulnerabilities, data corruption, or system failures. This affects all layers from hardware registers to high-level APIs, and can be particularly dangerous in security-critical code that depends on specific behavioral guarantees.

Risk

Expected behavior violations undermine the fundamental trust model of software development. Security code that depends on specific return values, error handling, or state changes may fail silently when components don't behave as documented. Third-party libraries with undocumented behavior variations can introduce vulnerabilities into otherwise secure code. Hardware behavior violations can enable privilege escalation when security controls operate differently than documented. The silent nature of these failures makes them difficult to detect through testing and particularly dangerous in production environments where assumptions are relied upon for security.

Solution

Thoroughly test that components behave according to their specifications. Use defensive programming to verify assumptions rather than trusting documentation alone. Implement runtime assertions to detect behavioral violations early. When using third-party components, create integration tests that verify expected behavior. Document any known behavioral deviations and their security implications. For security-critical code, verify behavior through multiple independent mechanisms. Consider using formal verification for hardware components where behavior must be guaranteed. Report behavioral violations to vendors and track them as potential security issues.

Common Consequences

ImpactDetails
OtherScope: Other

Quality Degradation - Software behaves incorrectly when components don't meet specifications, leading to unreliable operation.
OtherScope: Other

Varies by Context - Security implications depend on the specific behavioral violation and how the caller handles the unexpected behavior.

Example Code

Vulnerable Code

// Vulnerable: Relies on documented strncpy behavior
#include <string.h>
#include <stdio.h>

void vulnerable_string_copy(const char *input) {
    char buffer[64];

    // Vulnerable: strncpy behavior differs between implementations
    // Some guarantee null termination, others don't
    // Linux libc: does NOT null-terminate if source >= n
    // Some embedded libraries: always null-terminate

    strncpy(buffer, input, sizeof(buffer));

    // Vulnerable: Assumes buffer is null-terminated
    // If input >= 64 chars, buffer is NOT null-terminated on Linux
    printf("Copied: %s\n", buffer);  // May read beyond buffer
}

// Vulnerable: Relies on timeout behavior
int vulnerable_network_read(int socket, char *buffer, size_t len) {
    // Vulnerable: Assumes SO_RCVTIMEO works as documented
    // Some implementations ignore it for certain socket types
    // Some return different error codes than documented

    struct timeval tv = {.tv_sec = 5, .tv_usec = 0};
    setsockopt(socket, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof(tv));

    // Vulnerable: May block indefinitely if timeout not honored
    ssize_t bytes = read(socket, buffer, len);

    return bytes;
}
// Vulnerable: Hardware register with unexpected behavior
// Example based on actual RISC-V vulnerability

module vulnerable_csr_regfile (
    input wire clk,
    input wire rst_n,
    input wire [11:0] csr_addr,
    input wire [63:0] csr_wdata,
    input wire csr_we,
    output reg [63:0] csr_rdata
);

    reg [63:0] mie;      // Machine Interrupt Enable
    reg [63:0] utval;    // User Trap Value

    // Vulnerable: utval incorrectly affects mie calculation
    // Specification says utval should only store trap information
    // But this implementation uses it in mie_d assignment

    wire [63:0] mie_d = csr_wdata | utval;  // VULNERABLE: utval included

    always @(posedge clk or negedge rst_n) begin
        if (!rst_n) begin
            mie <= 64'b0;
        end else if (csr_we && csr_addr == 12'h304) begin
            mie <= mie_d;  // User can corrupt mie via utval
        end
    end

    // Attack: User writes to utval, then triggers mie write
    // Results in privilege escalation or DoS

endmodule
# Vulnerable: Relies on library's documented behavior
import json
import yaml

def vulnerable_parse_config(config_string, format_type):
    # Vulnerable: Assumes parsing behavior matches documentation

    if format_type == 'json':
        # Vulnerable: Some JSON parsers handle duplicates differently
        # Documentation may say "last value wins"
        # But implementation might use first value
        # {"admin": false, "admin": true} - which wins?
        config = json.loads(config_string)

    elif format_type == 'yaml':
        # Vulnerable: yaml.load() behavior changed between versions
        # Old: dangerous, allows code execution
        # New: may reject certain inputs
        config = yaml.load(config_string)  # Unsafe!

    # Vulnerable: Assumes 'get' returns None for missing keys
    # Some dict-like objects raise KeyError instead
    is_admin = config.get('is_admin', False)

    return config, is_admin
// Vulnerable: Relies on substring behavior
public class VulnerableStringHandler {

    public String extractToken(String input) {
        // Vulnerable: substring behavior changed in Java 7
        // Java 6: substring shares backing array (memory efficient, security issue)
        // Java 7+: substring creates new array (secure but different)

        String fullString = readLargeSecret();  // Contains sensitive data

        // Vulnerable on Java 6: token shares memory with fullString
        // Sensitive data remains accessible through token's backing array
        String token = fullString.substring(0, 32);

        fullString = null;  // Attempts to clear sensitive data
        // But on Java 6, data still accessible through token!

        return token;
    }

    // Vulnerable: Relies on hashCode behavior
    public boolean checkCache(String key) {
        // Vulnerable: String hashCode implementation may vary
        // Attack: craft strings with same hashCode to cause DoS

        // Java's hashCode is documented but consistent collisions
        // are possible, causing HashMap degradation
        return cache.containsKey(key);
    }
}

Fixed Code

// Fixed: Defensive string handling
#include <string.h>
#include <stdio.h>

void secure_string_copy(const char *input) {
    char buffer[64];

    // Fixed: Explicitly handle null termination
    size_t input_len = strlen(input);
    size_t copy_len = input_len < sizeof(buffer) - 1 ?
                      input_len : sizeof(buffer) - 1;

    memcpy(buffer, input, copy_len);
    buffer[copy_len] = '\0';  // Fixed: Always null-terminate

    printf("Copied: %s\n", buffer);
}

// Alternative: Use strlcpy where available
void secure_string_copy_strlcpy(const char *input) {
    char buffer[64];

    // Fixed: strlcpy always null-terminates
    size_t result = strlcpy(buffer, input, sizeof(buffer));

    if (result >= sizeof(buffer)) {
        // Fixed: Detect truncation
        log_warning("Input truncated from %zu to %zu", result, sizeof(buffer) - 1);
    }

    printf("Copied: %s\n", buffer);
}

// Fixed: Defensive timeout handling
int secure_network_read(int socket, char *buffer, size_t len) {
    struct pollfd pfd = {
        .fd = socket,
        .events = POLLIN
    };

    // Fixed: Use poll for reliable timeout
    int poll_result = poll(&pfd, 1, 5000);  // 5 second timeout

    if (poll_result == 0) {
        return -1;  // Timeout
    } else if (poll_result < 0) {
        return -2;  // Error
    }

    // Fixed: Now safe to read - data is available
    ssize_t bytes = read(socket, buffer, len);

    return bytes;
}
// Fixed: Hardware register with correct behavior
module secure_csr_regfile (
    input wire clk,
    input wire rst_n,
    input wire [11:0] csr_addr,
    input wire [63:0] csr_wdata,
    input wire csr_we,
    output reg [63:0] csr_rdata
);

    reg [63:0] mie;      // Machine Interrupt Enable
    reg [63:0] utval;    // User Trap Value

    // Fixed: mie calculation does not include utval
    // utval is only used for its specified purpose
    wire [63:0] mie_d = csr_wdata;  // FIXED: Only uses write data

    always @(posedge clk or negedge rst_n) begin
        if (!rst_n) begin
            mie <= 64'b0;
            utval <= 64'b0;
        end else if (csr_we) begin
            case (csr_addr)
                12'h304: mie <= mie_d;           // mie register
                12'h043: utval <= csr_wdata;     // utval register (separate)
            endcase
        end
    end

    // Fixed: User cannot corrupt mie via utval
    // Each register operates independently per specification

endmodule
# Fixed: Defensive parsing with behavior verification
import json
import yaml
from typing import Any, Dict, Tuple

def secure_parse_config(config_string: str, format_type: str) -> Tuple[Dict, bool]:
    config: Dict[str, Any] = {}

    if format_type == 'json':
        # Fixed: Use strict parsing mode
        config = json.loads(config_string, strict=True)

        # Fixed: Verify no duplicate keys
        # Custom decoder that detects duplicates
        def detect_duplicates(pairs):
            seen = set()
            result = {}
            for key, value in pairs:
                if key in seen:
                    raise ValueError(f"Duplicate key in JSON: {key}")
                seen.add(key)
                result[key] = value
            return result

        config = json.loads(config_string, object_pairs_hook=detect_duplicates)

    elif format_type == 'yaml':
        # Fixed: Use safe loader
        config = yaml.safe_load(config_string)

        # Fixed: Verify it's a dict
        if not isinstance(config, dict):
            raise ValueError("Config must be a dictionary")

    # Fixed: Explicit default handling
    if 'is_admin' not in config:
        is_admin = False
    else:
        is_admin = bool(config['is_admin'])

    return config, is_admin

# Fixed: Verify behavior at startup
def verify_json_behavior():
    """Verify JSON parsing behavior matches expectations."""
    test_input = '{"key": 1, "key": 2}'
    result = json.loads(test_input)

    # Document actual behavior
    if result['key'] == 2:
        print("JSON parser uses last-value-wins for duplicates")
    else:
        print("JSON parser uses first-value-wins for duplicates")

    # Our secure parser should reject this
    try:
        secure_parse_config(test_input, 'json')
        raise AssertionError("Expected duplicate key rejection")
    except ValueError:
        print("Duplicate key rejection working correctly")
// Fixed: Defensive string handling
import java.security.SecureRandom;
import java.util.Arrays;

public class SecureStringHandler {

    public String extractToken(String input) {
        byte[] fullData = readLargeSecretAsBytes();

        // Fixed: Create independent copy
        byte[] tokenBytes = Arrays.copyOfRange(fullData, 0, 32);

        // Fixed: Clear original data
        Arrays.fill(fullData, (byte) 0);

        // Fixed: Create new String from independent byte array
        String token = new String(tokenBytes, StandardCharsets.UTF_8);

        // Fixed: Clear token bytes
        Arrays.fill(tokenBytes, (byte) 0);

        return token;
    }

    // Fixed: Use collision-resistant key handling
    public boolean checkCache(String key) {
        // Fixed: Use TreeMap for guaranteed O(log n) regardless of hashCode
        // Or use key hashing that's collision-resistant
        String hashedKey = computeSecureHash(key);
        return cache.containsKey(hashedKey);
    }

    private String computeSecureHash(String input) {
        try {
            MessageDigest md = MessageDigest.getInstance("SHA-256");
            byte[] hash = md.digest(input.getBytes(StandardCharsets.UTF_8));
            return Base64.getEncoder().encodeToString(hash);
        } catch (NoSuchAlgorithmException e) {
            throw new RuntimeException("SHA-256 not available", e);
        }
    }
}

CVE Examples

  • CVE-2003-0187 - Timeout functionality behaved inconsistently, causing security checks to be bypassed.
  • CVE-2003-0465 - strncpy behavior differences between Linux kernel and libc led to security issues.
  • CVE-2005-3265 - Third-party library lacked expected buffer overflow protection, causing vulnerability in calling code.

References

  1. MITRE Corporation. "CWE-440: Expected Behavior Violation." https://cwe.mitre.org/data/definitions/440.html
  2. RISC-V Foundation. "RISC-V Privileged Architecture Specification."