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
| Impact | Details |
|---|---|
| Integrity | Scope: Integrity Modify Application Data - Sentinel addition generally causes data structures to not work properly by truncating data at the inserted sentinel point. |
| Access Control | Scope: 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
- MITRE Corporation. "CWE-464: Addition of Data Structure Sentinel." https://cwe.mitre.org/data/definitions/464.html
- CERT C Secure Coding Standard. "STR03-C. Do not inadvertently truncate a string."
- OWASP. "Embedding Null Code." https://owasp.org/www-community/attacks/Embedding_Null_Code