PHP Bugs 21 to 30 — Common Mistakes Every PHP Developer Must Know

If you've been writing PHP for a while, you've probably hit a few of these without realizing what was actually happening under the hood. This is Part 3 of a series I've been running on CodePractice — Part 1 covered type juggling and variable scoping, Part 2 covered form handling and session quirks. This batch is heavier. A few of these are genuine security vulnerabilities. One can take your server down under load. A couple corrupt data without throwing a single error.
Original post with the full quick-reference table and FAQ: PHP Bugs 21 to 30 — Common Mistakes Every PHP Developer Must Know
Let's go through them.
#21 — strlen() on multibyte text
echo strlen("नमस्ते"); // 18, not 6
strlen() counts bytes, not characters. UTF-8 encodes non-Latin characters as 2–3 bytes each. Swap in mb_strlen($string, 'UTF-8') — and once you're handling multibyte input, switch the whole family: mb_substr, mb_strtolower, mb_strpos.
#22 — Trusting an API response blindly
$response = file_get_contents("https://api.example.com/data");
\(data = json_decode(\)response);
echo $data->name; // null, and you have no idea why
Three things can fail here and none are checked: the request itself, JSON validity, and response structure. Check $response === false, decode with true for an associative array, and check json_last_error().
#23 — Cookies dying on browser close
No expiry argument means a session cookie. Pass time() + 86400 * 30 for persistence, or use the options array on PHP 7.3+ to set secure, httponly, and samesite at the same time.
#24 — File upload validation via extension
if (pathinfo($file, PATHINFO_EXTENSION) == 'jpg') {
move_uploaded_file(...);
}
Renaming shell.php to shell.jpg defeats this instantly. Use finfo_file() to check actual file bytes, randomize the stored filename, and disable PHP execution in the uploads directory via .htaccess as a second layer.
#25 — number_format() on comma-formatted strings
$price = "1,299.00";
echo number_format($price * 1.18); // wrong — PHP reads "1" up to the comma
Strip the comma, floatval() it, then multiply. Don't store prices as formatted strings — formatting belongs at the display layer only.
#26 — static properties shared across instances
class Cart {
static $items = []; // shared by every Cart object
}
This is the classic shopping-cart bug — add an item to one cart, it shows up in another. Use a regular instance property unless the value genuinely belongs to the class itself (an object counter, shared config).
#27 — Execution continuing after a redirect header
if (!$isAdmin) {
header("Location: login.php");
deleteAllUsers(); // still executes!
}
header() sends a redirect instruction to the browser — it does not stop the script. Every line after it still runs server-side. Always pair header("Location: ...") with exit(). This one has real security implications: a raw HTTP request (not a browser following the redirect) can trigger the code that should've been blocked.
#28 — SQL injection via unsanitized search input
\(sql = "SELECT * FROM products WHERE name LIKE '%\)search%'";
Prepared statements fix this completely:
\(stmt = \)pdo->prepare("SELECT * FROM products WHERE name LIKE ?");
\(stmt->execute(['%' . \)search . '%']);
Note the % wildcards go outside the placeholder in PHP, not inside the query string.
#29 — Loading large files entirely into memory
$data = file_get_contents("large_export.csv"); // 500MB file = 500MB RAM
Stream instead:
$handle = fopen("large_export.csv", "r");
while ((\(line = fgets(\)handle)) !== false) {
processRow(str_getcsv($line));
}
fclose($handle);
Memory stays flat regardless of file size. For very large imports, LOAD DATA INFILE in MySQL is faster than PHP either way.
#30 — PDO failing silently
PDO defaults to silent mode — failed queries return false, no exception. Fix it at connection time:
\(pdo = new PDO(\)dsn, \(user, \)pass, [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
PDO::ATTR_EMULATE_PREPARES => false,
]);
Catch PDOException, log the real message, and never echo it directly to users.
The pattern
Nearly every bug here comes from PHP assuming something worked when it didn't — and quietly letting you be wrong. The fix is consistent: validate assumptions, check return values, set explicit error modes, and treat all external input as untrusted.
Full breakdown with the wrong/correct code for each bug plus a quick-reference table is on the original post: PHP Bugs 21 to 30 — Common Mistakes Every PHP Developer Must Know
Originally published on CodePractice.
#php #webdev #backend #security #programming





