If you've been following this series — Part 1 (Bugs #1–10) covered type juggling and variable scoping, Part 2 (Bugs #11–20) tackled form handling and session quirks — now Part 3 is where things get genuinely dangerous.
Bugs #21 to #30 aren't just annoying syntax mistakes. Some are active security vulnerabilities. A few will crash your server under load. Others silently corrupt data without throwing a single error.
Let's go through each one.
Bug #21 — strlen() Giving Wrong Count for Multibyte Text
// ❌ Wrong
echo strlen("नमस्ते"); // Output: 18 — but it has only 6 characters!
strlen() counts bytes, not characters. In UTF-8, each Hindi character takes 3 bytes. Six characters × 3 bytes = 18.
// ✅ Correct
echo mb_strlen("नमस्ते", 'UTF-8'); // Output: 6
Once you're working with multibyte text, switch the entire function family:
-
substr()→mb_substr() -
strtolower()→mb_strtolower() -
strpos()→mb_strpos()
Any form that accepts multilingual input needs mb_ functions.
Bug #22 — API Response Returning Null
// ❌ Wrong — no error checking at all
$response = file_get_contents("https://api.example.com/data");
$data = json_decode($response);
echo $data->name; // Null!
Three things can fail here and this code catches none of them:
-
file_get_contents()might have failed silently - The response might not be valid JSON
- The structure might be different from what you expected
// ✅ Correct
$response = file_get_contents("https://api.example.com/data");
if ($response === false) {
die("API request failed");
}
$data = json_decode($response, true); // true = associative array
if (json_last_error() !== JSON_ERROR_NONE) {
die("JSON decode error: " . json_last_error_msg());
}
echo $data['name'];
In production, replace die() with proper logging. Never expose raw API errors to users.
Bug #23 — Cookie Disappears After Browser Close
// ❌ Wrong — creates a session cookie (lives in memory only)
setcookie("user", "John");
No expiry = browser memory only. Tab closes → cookie gone.
// ✅ Correct — with security flags (PHP 7.3+)
setcookie("user", "John", [
'expires' => time() + 86400 * 30, // 30 days
'secure' => true, // HTTPS only
'httponly' => true, // No JS access (XSS protection)
'samesite' => 'Strict'
]);
The httponly flag is crucial — it prevents JavaScript from reading the cookie and protects against XSS attacks stealing session data.
Bug #24 — File Upload Accepting PHP Files 🚨
This isn't just a bug. It's a critical security vulnerability.
// ❌ Dangerously wrong
if (pathinfo($file, PATHINFO_EXTENSION) == 'jpg') {
move_uploaded_file(...); // shell.php renamed to shell.jpg bypasses this
}
Renaming shell.php to shell.jpg takes two seconds. Extension checks are useless for security.
// ✅ Correct — check actual file content
$allowed_types = ['image/jpeg', 'image/png', 'image/gif', 'image/webp'];
$finfo = finfo_open(FILEINFO_MIME_TYPE);
$mime = finfo_file($finfo, $_FILES['file']['tmp_name']);
finfo_close($finfo);
if (in_array($mime, $allowed_types)) {
$safe_name = uniqid() . '_' . time() . '.jpg'; // Never keep original filename
move_uploaded_file($_FILES['file']['tmp_name'], 'uploads/' . $safe_name);
} else {
die("Only image files are allowed.");
}
Extra layer: Disable PHP execution in your uploads folder via .htaccess:
<Directory /var/www/html/uploads>
php_flag engine off
</Directory>
Even if a PHP file sneaks in — it won't execute.
Bug #25 — number_format() Giving Wrong Calculation Results
// ❌ Wrong
$price = "1,299.00";
echo number_format($price * 1.18); // Calculates on 1, not 1299!
PHP stops reading the string at the comma and converts "1,299.00" to the float 1. Your 18% tax runs on ₹1.00 instead of ₹1299.00.
// ✅ Correct — strip comma first, then calculate
$price = floatval(str_replace(',', '', "1,299.00")); // 1299.00
echo number_format($price * 1.18, 2); // 1532.82
Rule: Never store prices as formatted strings in your database. Store raw numbers, apply number_format() only at the display layer.
Bug #26 — Class Variable Shared Across All Objects
// ❌ Wrong
class Cart {
static $items = []; // Shared by ALL Cart objects
}
$cart1 = new Cart();
$cart1::$items[] = "Laptop";
$cart2 = new Cart();
print_r($cart2::$items); // ["Laptop"] — What?!
A static property belongs to the class, not to individual instances. Every Cart object shares the same $items.
// ✅ Correct
class Cart {
public $items = []; // Each object gets its own copy
}
$cart1 = new Cart();
$cart1->items[] = "Laptop";
$cart2 = new Cart();
print_r($cart2->items); // [] — Correct
Use static only for values that genuinely belong to the class as a whole — like a shared counter or config, not per-user data.
Bug #27 — Code Still Runs After a Redirect
// ❌ Wrong — and a serious security hole
if (!$isAdmin) {
header("Location: login.php");
deleteAllUsers(); // This STILL runs on the server!
}
header() sends an HTTP instruction to the browser. It does not stop the PHP script. The browser redirects — but every line after keeps executing on the server.
// ✅ Correct
if (!$isAdmin) {
header("Location: login.php");
exit(); // Script stops here. Done.
}
An attacker who understands HTTP can send a raw request and receive the full server response before the redirect happens. Always pair header("Location: ...") with exit(). No exceptions.
Bug #28 — Search Query Vulnerable to SQL Injection
// ❌ Wrong — never do this
$search = $_GET['q'];
$sql = "SELECT * FROM products WHERE name LIKE '%$search%'";
User types % → gets every product. Types ' → SQL error. Types ' OR '1'='1 → manipulates your entire query.
// ✅ Correct — prepared statements
$search = $_GET['q'];
$stmt = $pdo->prepare("SELECT * FROM products WHERE name LIKE ?");
$stmt->execute(['%' . $search . '%']);
$results = $stmt->fetchAll(PDO::FETCH_ASSOC);
The ? placeholder keeps user input completely separate from query logic. It can never modify the query structure — no matter what characters the user types.
Note:
%wildcards go in PHP outside the placeholder, not inside the query string.
Bug #29 — Large Files Crashing PHP With Memory Errors
// ❌ Wrong — loads entire file into RAM
$data = file_get_contents("large_export.csv"); // 500MB file = 500MB RAM. Fatal.
file_get_contents() loads the entire file into a PHP string. Most PHP configs cap memory at 128–256MB. Your script crashes before processing a single row.
// ✅ Correct — stream line by line
$handle = fopen("large_export.csv", "r");
if ($handle !== false) {
while (($line = fgets($handle)) !== false) {
$row = str_getcsv($line);
processRow($row); // One row at a time, flat memory usage
}
fclose($handle);
}
fgets() reads one line at a time — memory stays nearly flat regardless of file size. For huge database imports, look into MySQL's LOAD DATA INFILE — it's significantly faster than PHP for bulk inserts.
Bug #30 — PDO Not Reporting Database Errors
// ❌ Wrong — silent failure by default
$pdo = new PDO("mysql:host=localhost;dbname=mydb", $user, $pass);
$stmt = $pdo->query("SELECT * FROM nonexistent_table");
// Returns false. No exception. No warning. Nothing.
PDO's default mode is silent. Errors return false and execution continues. If you're not manually checking every return value, failures disappear without a trace.
// ✅ Correct — set error mode at connection time
try {
$pdo = new PDO(
"mysql:host=localhost;dbname=mydb",
$user,
$pass,
[
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
PDO::ATTR_EMULATE_PREPARES => false,
]
);
} catch (PDOException $e) {
error_log($e->getMessage()); // Log the real error
die("A database error occurred. Please try again.");
}
Production rule: Never echo $e->getMessage() to users. It may expose table names, column names, or connection details. Log it privately, show a generic message publicly.
Quick Reference
| Bug | The Mistake | The Fix |
|---|---|---|
| #21 |
strlen() on multibyte text |
mb_strlen() with encoding |
| #22 | API response null, no validation | Check false, use json_last_error()
|
| #23 | Cookie deleted on browser close | Pass expiry time to setcookie()
|
| #24 | File upload accepting PHP files | Check MIME type with finfo_file()
|
| #25 | Comma-formatted price wrong calc | Strip comma, then floatval()
|
| #26 | All objects share same static items | Remove static, use instance property |
| #27 | Code runs after redirect | Add exit() after every header()
|
| #28 | SQL injection via search input | Prepared statements with PDO |
| #29 | Memory crash on large CSV | Stream with fgets() line by line |
| #30 | PDO errors invisible | Set ERRMODE_EXCEPTION at connection |
The Common Thread
Every bug here follows the same pattern: assuming something worked when it didn't. PHP quietly lets you be wrong — no warning, no crash, just subtly broken behaviour in production.
The fix is the same for all of them: be explicit. Check return values. Set error modes. Validate inputs. Treat anything from outside your code as untrusted until proven otherwise.
Full article with deeper explanations: codepractice.in
Next in the series: PHP session security, password hashing, and CSRF protection.
Top comments (0)