Addition of Data Structure Sentinel

Description

Addition of Data Structure Sentinel is a vulnerability where the accidental addition of a data-structure sentinel can cause serious programming logic problems. Data-structure sentinels such as null characters in strings or special markers in linked lists control data structure integrity and mark boundaries. When user input can introduce sentinel values into data structures, attackers can truncate data, bypass validation, or cause other unexpected behavior by inserting these control characters.

Risk

Inadvertent sentinel addition enables attackers to manipulate data structure behavior. Injecting null bytes into strings causes premature termination, truncating data or bypassing security checks that examine content after the null. In file paths, null bytes can truncate paths to access unintended files. In databases, sentinel injection can corrupt data structures. The high likelihood of exploitation combined with the ability to bypass security controls makes this a significant vulnerability. Attackers can use sentinel injection to evade input filters or manipulate application logic.

Solution

Encapsulate users from sentinel interaction through proper abstraction layers. Validate all input to exclude sentinel values before incorporating data into structures. Implement proper error checking to prevent inadvertent sentinel insertion. Use abstraction libraries that mask risky APIs and handle sentinels internally. For strings, sanitize or reject null bytes in user input. Consider using length-prefixed data structures instead of sentinel-terminated ones. Apply OS-level preventative functionality where available.

Common Consequences

ImpactDetails
IntegrityScope: Integrity

Modify Application Data - Sentinel addition generally causes data structures to not work properly by truncating data at the inserted sentinel point.
Access ControlScope: Access Control

Bypass Protection Mechanism - Security checks that examine data after an injected sentinel may be bypassed as they won't see the remaining content.

Example Code

Vulnerable Code

// Vulnerable: User input containing null byte
#include <stdio.h>
#include <string.h>

void vulnerable_string_input() {
    char buffer[256];
    int c;
    int i = 0;

    // Vulnerable: fgetc can read null bytes
    while ((c = fgetc(stdin)) != EOF && c != '\n' && i < 255) {
        buffer[i++] = c;  // Null byte (0x00) can be inserted here
    }
    buffer[i] = '\0';

    // If user inputs "admin\x00ignore_this"
    // buffer contains: "admin\0ignore_this\0"
    // strlen(buffer) returns 5
    // strcmp(buffer, "admin") returns 0 - match!
    // But intended input was "admin\x00ignore_this"

    printf("Input: %s\n", buffer);  // Only prints "admin"
}

// Vulnerable: Path traversal via null byte injection
void vulnerable_file_access(const char *user_filename) {
    char filepath[512];
    snprintf(filepath, sizeof(filepath), "/var/data/%s.txt", user_filename);

    // Attacker provides: "../../etc/passwd\x00"
    // filepath becomes: "/var/data/../../etc/passwd\x00.txt"
    // fopen sees: "/var/data/../../etc/passwd"
    // The ".txt" extension is ignored due to null byte

    FILE *f = fopen(filepath, "r");
    if (f) {
        // Reads /etc/passwd instead of intended file
        char content[1024];
        fread(content, 1, sizeof(content), f);
        fclose(f);
    }
}
<?php
// Vulnerable: PHP null byte injection
function vulnerable_include($page) {
    // Intended: only include .php files from pages directory
    $file = "pages/" . $page . ".php";

    // Attacker provides: "../../../../etc/passwd\x00"
    // $file becomes: "pages/../../../../etc/passwd\x00.php"
    // PHP (older versions) sees: "pages/../../../../etc/passwd"

    if (file_exists($file)) {
        include($file);  // Includes /etc/passwd
    }
}

// Vulnerable: Extension validation bypass
function vulnerable_upload($filename, $content) {
    $allowed_extensions = array('jpg', 'png', 'gif');

    // Extract extension
    $ext = pathinfo($filename, PATHINFO_EXTENSION);

    // Attacker provides: "malware.php\x00.jpg"
    // pathinfo sees extension as "jpg" (after null)
    // But filesystem may save as "malware.php"

    if (in_array(strtolower($ext), $allowed_extensions)) {
        file_put_contents("uploads/" . $filename, $content);
        // Malicious PHP file uploaded
    }
}
?>
# Vulnerable: Sentinel addition in data processing
class VulnerableDataProcessor:
    RECORD_SEPARATOR = '\x1E'  # ASCII Record Separator

    def add_record(self, data):
        # Vulnerable: User can inject record separator
        # This splits their data into multiple records
        self.buffer += data + self.RECORD_SEPARATOR

        # Attacker provides: "value1\x1Emalicious_record"
        # Creates two records instead of one

    def validate_input(self, user_input):
        # Vulnerable: Validation happens after null bytes
        # Attacker: "safe_input\x00<script>alert(1)</script>"

        if "script" not in user_input.lower():
            return True  # Passes because comparison uses C strings

        # In some contexts, the part after null is still processed
        return False

    def create_command(self, user_arg):
        # Vulnerable: Null byte truncates command
        command = f"process --safe-mode --input={user_arg}"

        # Attacker: "file.txt\x00--unsafe-mode"
        # May truncate at null, or may pass through to shell
        # depending on how command is executed

        return command
// Vulnerable: Java with native calls can be affected
public class VulnerableNativeHandler {

    // Native method that uses C strings
    private native void processFile(String filename);

    public void handleUserFile(String userFilename) {
        // Vulnerable: Java String can contain null bytes
        // Native code using strlen/strcmp will truncate at null

        // User provides: "safe.txt\0../../etc/passwd"
        // Java sees full string
        // Native C code sees: "safe.txt"

        if (userFilename.endsWith(".txt")) {
            processFile(userFilename);
            // Native code may process wrong file
        }
    }

    // Vulnerable: Database sentinel injection
    public void storeData(String key, String value) {
        // If database uses special byte sequences as delimiters
        // attacker can inject them to corrupt storage

        String record = key + "\t" + value;  // Tab as separator

        // Attacker value: "data\tanother_key\tattacker_value"
        // Creates multiple records instead of one

        database.write(record);
    }
}

Fixed Code

// Fixed: Reject null bytes in user input
#include <stdio.h>
#include <string.h>

int secure_string_input(char *buffer, size_t buffer_size) {
    int c;
    size_t i = 0;

    while ((c = fgetc(stdin)) != EOF && c != '\n' && i < buffer_size - 1) {
        // Fixed: Reject null bytes
        if (c == '\0') {
            fprintf(stderr, "Error: Null byte in input not allowed\n");
            buffer[0] = '\0';
            return -1;
        }
        buffer[i++] = c;
    }
    buffer[i] = '\0';

    return 0;
}

// Fixed: Validate path components without null bytes
int secure_file_access(const char *user_filename) {
    // Fixed: Check for null bytes in input
    if (memchr(user_filename, '\0', strlen(user_filename) + 1) !=
        user_filename + strlen(user_filename)) {
        // This check detects if there are embedded nulls
        // Actually, strlen stops at first null, so use different approach
    }

    // Fixed: Better approach - validate character by character
    for (size_t i = 0; user_filename[i] != '\0'; i++) {
        // Fixed: Reject null bytes and path traversal
        if (user_filename[i] == '\0' ||
            user_filename[i] == '/' ||
            user_filename[i] == '\\') {
            return -1;
        }
    }

    // Fixed: Use safe path construction
    char filepath[512];
    int written = snprintf(filepath, sizeof(filepath),
                           "/var/data/%s.txt", user_filename);

    if (written < 0 || written >= sizeof(filepath)) {
        return -1;  // Path too long or error
    }

    // Fixed: Verify no path traversal occurred
    char resolved[PATH_MAX];
    if (realpath(filepath, resolved) == NULL) {
        return -1;
    }

    if (strncmp(resolved, "/var/data/", 10) != 0) {
        return -1;  // Escaped intended directory
    }

    FILE *f = fopen(resolved, "r");
    // ...
    return 0;
}
<?php
// Fixed: Null byte sanitization
function secure_include($page) {
    // Fixed: Remove null bytes
    $page = str_replace(chr(0), '', $page);

    // Fixed: Whitelist allowed pages
    $allowed_pages = array('home', 'about', 'contact', 'products');

    if (!in_array($page, $allowed_pages)) {
        die('Invalid page');
    }

    $file = "pages/" . $page . ".php";
    include($file);
}

// Fixed: Secure file upload
function secure_upload($filename, $content) {
    // Fixed: Remove null bytes from filename
    $filename = str_replace(chr(0), '', $filename);

    // Fixed: Generate safe filename
    $safe_name = preg_replace('/[^a-zA-Z0-9_.-]/', '', $filename);

    // Fixed: Verify extension using actual file content
    $finfo = new finfo(FILEINFO_MIME_TYPE);
    $mime = $finfo->buffer($content);

    $allowed_mimes = array(
        'image/jpeg' => 'jpg',
        'image/png' => 'png',
        'image/gif' => 'gif'
    );

    if (!isset($allowed_mimes[$mime])) {
        die('Invalid file type');
    }

    // Fixed: Use detected extension
    $safe_filename = pathinfo($safe_name, PATHINFO_FILENAME) . '.' . $allowed_mimes[$mime];

    file_put_contents("uploads/" . $safe_filename, $content);
}
?>
# Fixed: Sentinel sanitization in data processing
class SecureDataProcessor:
    RECORD_SEPARATOR = '\x1E'
    FORBIDDEN_BYTES = ['\x00', '\x1E', '\x1F']  # Null and record separators

    def sanitize_input(self, data):
        """Remove or escape sentinel characters from input."""
        sanitized = data
        for byte in self.FORBIDDEN_BYTES:
            sanitized = sanitized.replace(byte, '')
        return sanitized

    def add_record(self, data):
        # Fixed: Sanitize before adding
        clean_data = self.sanitize_input(data)
        self.buffer += clean_data + self.RECORD_SEPARATOR

    def validate_input(self, user_input):
        # Fixed: Check for null bytes first
        if '\x00' in user_input:
            raise ValueError("Null bytes not allowed in input")

        # Fixed: Now safe to check content
        if "script" in user_input.lower():
            return False
        return True

    def create_command(self, user_arg):
        # Fixed: Reject null bytes
        if '\x00' in user_arg:
            raise ValueError("Null bytes not allowed")

        # Fixed: Use parameterized execution
        import subprocess
        return subprocess.run(
            ['process', '--safe-mode', f'--input={user_arg}'],
            capture_output=True
        )
// Fixed: Null byte handling in Java
public class SecureNativeHandler {

    private native void processFile(String filename);

    public void handleUserFile(String userFilename) throws SecurityException {
        // Fixed: Check for null bytes
        if (userFilename.indexOf('\0') != -1) {
            throw new SecurityException("Null bytes not allowed in filename");
        }

        // Fixed: Validate filename characters
        if (!userFilename.matches("^[a-zA-Z0-9_.-]+\\.txt$")) {
            throw new SecurityException("Invalid filename format");
        }

        // Fixed: Resolve path and verify it's in allowed directory
        Path basePath = Paths.get("/var/data").toRealPath();
        Path filePath = basePath.resolve(userFilename).normalize();

        if (!filePath.startsWith(basePath)) {
            throw new SecurityException("Path traversal detected");
        }

        processFile(filePath.toString());
    }

    // Fixed: Proper encoding for database storage
    public void storeData(String key, String value) throws SecurityException {
        // Fixed: Validate no control characters
        if (containsControlChars(key) || containsControlChars(value)) {
            throw new SecurityException("Control characters not allowed");
        }

        // Fixed: Use proper escaping or parameterized storage
        PreparedStatement stmt = connection.prepareStatement(
            "INSERT INTO data (key, value) VALUES (?, ?)"
        );
        stmt.setString(1, key);
        stmt.setString(2, value);
        stmt.executeUpdate();
    }

    private boolean containsControlChars(String s) {
        for (int i = 0; i < s.length(); i++) {
            char c = s.charAt(i);
            if (c < 32 && c != '\t' && c != '\n' && c != '\r') {
                return true;
            }
        }
        return false;
    }
}

CVE Examples

No specific CVEs are listed in the MITRE database for this CWE. However, the pattern is heavily documented in:

  • Null byte injection attacks (PHP file inclusion, path traversal)
  • CERT C: STR03-C. Do not inadvertently truncate a string
  • CERT C: STR06-C. Do not assume that strtok() leaves the parse string unchanged

References

  1. MITRE Corporation. "CWE-464: Addition of Data Structure Sentinel." https://cwe.mitre.org/data/definitions/464.html
  2. CERT C Secure Coding Standard. "STR03-C. Do not inadvertently truncate a string."
  3. OWASP. "Embedding Null Code." https://owasp.org/www-community/attacks/Embedding_Null_Code