PHP Bugs 21 to 30 - Common PHP Debugging Mistakes Every Backend Developer Must Know Including SQL Injection strlen PDO Errors and File Upload Security in PHP CodePractice

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

CodePractice Blog Author

Published By

Bikki Singh
  • PHP

  • 3 Views

If you've been following this series, you already know the drill. PHP Bugs #1–10 covered type juggling and variable scoping. PHP Bugs #11–20 tackled form handling and session quirks. Now in Part 3, things get more serious.

Bugs #21 to #30 aren't just annoying syntax mistakes — some of them are actual security vulnerabilities. A few will crash your server under load. Others silently corrupt data without throwing a single error. These are the kinds of bugs that slip past beginners and even intermediate developers who haven't hit them in production yet.

Let's go through each one.

Bug #21 — strlen() Giving Wrong Count for Multibyte Text

The problem: You're counting characters in a Hindi, Arabic, or any non-ASCII string and getting a completely wrong number.

// ❌ Wrong
echo strlen("नमस्ते"); // Output: 18

"नमस्ते" has 6 characters. So why does PHP say 18?

Because strlen() counts bytes, not characters. In UTF-8 encoding, each Hindi character takes 2–3 bytes. So 6 characters × 3 bytes = 18. The function isn't broken — it's just doing something different from what you expected.

// ✅ Correct
echo mb_strlen("नमस्ते", 'UTF-8'); // Output: 6

The mb_ prefix stands for multibyte. The mb_strlen() function understands character encoding and counts actual characters, not raw bytes.

Why it matters in real projects: If you're validating that a name field is under 50 characters, strlen() will reject valid short names just because they use non-Latin characters. Any form that accepts multilingual input needs mb_strlen().

This also applies to substr()mb_substr(), strtolower()mb_strtolower(), and strpos()mb_strpos(). Once you're working with multibyte text, switch the whole family.

Related Tutorial: PHP String Functions — The Complete mb_ Reference Guide


Bug #22 — API Response Returning Null

The problem: You call an external API, decode the JSON, and try to access a property — but you get null.

// ❌ Wrong
$response = file_get_contents("https://api.example.com/data");
$data = json_decode($response);
echo $data->name; // Null!

There are three separate things that could go wrong here, and this code catches none of them:

  1. file_get_contents() might have failed — wrong URL, network timeout, SSL error
  2. The response might not be valid JSON
  3. The JSON 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'];

Pass true as the second argument to json_decode() to get an associative array instead of an object. Arrays are easier to work with and don't throw errors on undefined keys in the same way.

In production: Replace die() with proper logging and a graceful error response to the user. Never expose raw error messages from third-party APIs.


Bug #23 — Cookie Disappears After Browser Close

The problem: You set a cookie to remember a user, but it's gone as soon as they close the browser.

// ❌ Wrong
setcookie("user", "John"); // No expiry set

When you don't provide an expiration time, PHP creates a session cookie. Session cookies live in browser memory only — the moment the browser closes, they're wiped.

// ✅ Correct
setcookie("user", "John", time() + (86400 * 30)); // 30 days

86400 is the number of seconds in a day. Multiply by 30 and you get a cookie that persists for a month.

For modern PHP (7.3+), use the options array — it's cleaner and lets you set security flags:

setcookie("user", "John", [
    'expires'  => time() + 86400 * 30,
    'secure'   => true,   // HTTPS only
    'httponly' => true,   // No JavaScript access
    'samesite' => 'Strict'
]);
httponly is important — it prevents JavaScript from reading the cookie, which protects against XSS attacks stealing session data.

Related Tutorial: PHP Cookies and Sessions — Security Best Practices


Bug #24 — Image Upload Accepting PHP Files

Stop. This isn't just a bug — this is a critical security vulnerability. If an attacker uploads a PHP file disguised as an image and your server executes it, they own your server.

// ❌ Dangerously wrong
if (pathinfo($file, PATHINFO_EXTENSION) == 'jpg') {
    move_uploaded_file(...);
}

Renaming shell.php to shell.jpg takes two seconds. Extension checks are completely 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.");
}

finfo_file() reads the actual bytes of the file to determine its real type — it doesn't trust the extension or the Content-Type header from the browser.

Extra layer of defense: Add this to your uploads folder's .htaccess to block PHP execution entirely:

<Directory /var/www/html/uploads>
    php_flag engine off
</Directory>

Even if someone gets a PHP file in there somehow, it won't execute.

Related Tutorial: Secure File Upload in PHP — The Complete Security Checklist


Bug #25 — number_format() Giving Wrong Calculation Results

The problem: You're calculating a price with tax but getting the completely wrong answer.

// ❌ Wrong
$price = "1,299.00";
echo number_format($price * 1.18); // Wrong result!

When PHP converts the string "1,299.00" to a float for multiplication, it stops at the comma. So it reads 1 — not 1299. Your 18% tax calculation runs on 1.00, not 1299.00.

// ✅ Correct
$price = floatval(str_replace(',', '', "1,299.00")); // 1299.00
echo number_format($price * 1.18, 2); // 1532.82

The order matters: strip the comma first, convert to float, do the math, then format for display.

Real-world tip: Never store prices as formatted strings in your database. Store raw numbers (integer paise/cents or decimal) and only apply number_format() at the display layer. Formatted strings are for humans to read, not for code to calculate with.


Bug #26 — Class Variable Shared Across All Objects

The problem: You add an item to one shopping cart object, and it shows up in a completely separate cart.

// ❌ Wrong
class Cart {
    static $items = []; // All objects share this!
}

$cart1 = new Cart();
$cart1::$items[] = "Laptop";

$cart2 = new Cart();
print_r($cart2::$items); // ["Laptop"] — Wait, what?

static property belongs to the class, not to individual objects. No matter how many Cart objects you create, they all read from and write to the same $items array.

// ✅ 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, empty

When should you actually use static? When the value genuinely belongs to the class as a whole — like a counter tracking how many objects have been created, or a shared configuration value. For anything that should be independent per object (cart contents, user data, form state), use regular instance properties.

Related Tutorial: PHP OOP Deep Dive — Static vs Instance Properties and Methods


Bug #27 — Code Still Runs After a Redirect

The problem: You redirect unauthorized users to the login page, but the restricted code still executes on the server.

// ❌ Wrong — and a serious security hole
if (!$isAdmin) {
    header("Location: login.php");
    // PHP doesn't stop here!
    deleteAllUsers(); // This still runs!
}

header() sends an HTTP header to the browser telling it to go somewhere else. It does not stop the PHP script. The browser redirects — but on the server, every line after that header() call still executes.

// ✅ Correct
if (!$isAdmin) {
    header("Location: login.php");
    exit(); // Script stops here. Done.
}

exit() immediately halts script execution. Nothing after it runs.

This is a mistake that looks harmless in development because you're following the redirect and never see the result of the subsequent code. In production, an attacker who understands HTTP can send a raw request and receive the full response before the redirect kicks in — seeing data or triggering actions that should have been blocked.

Always pair header("Location: ...") with exit(). No exceptions.


Bug #28 — Search Query Breaking With Special Characters (SQL Injection Risk)

The problem: When users type %, _, or a single quote into your search box, results break — or worse.

// ❌ Wrong
$search = $_GET['q'];
$sql = "SELECT * FROM products WHERE name LIKE '%$search%'";

If a user searches for %, they get every single product in your database. If they type ', you get a SQL error. And if they know what they're doing, they can type something like ' OR '1'='1 and manipulate your query entirely.

// ✅ Correct — Prepared statement
$search = $_GET['q'];
$stmt = $pdo->prepare("SELECT * FROM products WHERE name LIKE ?");
$stmt->execute(['%' . $search . '%']);
$results = $stmt->fetchAll(PDO::FETCH_ASSOC);

With a prepared statement, the ? is a safe placeholder. The user input is passed separately and treated purely as data — it can never modify the query's structure.

One detail worth noting: the % wildcards are added in PHP outside the placeholder, not inside. This is intentional — the database handles the ? value as a literal string, so if you put %?% in the query, it wouldn't work as expected.


Bug #29 — Large Files Crashing PHP With Memory Errors

The problem: Processing a large CSV file throws Fatal error: Allowed memory size exhausted.

// ❌ Wrong
$data = file_get_contents("large_export.csv"); // Entire file loaded into RAM
// Process it...

file_get_contents() reads the entire file into a PHP string in memory. A 500MB file = 500MB of RAM consumed instantly. Most PHP configurations have a memory limit of 128MB or 256MB — your script crashes before it even starts doing anything useful.

// ✅ Correct — Stream it line by line
$handle = fopen("large_export.csv", "r");

if ($handle !== false) {
    while (($line = fgets($handle)) !== false) {
        $row = str_getcsv($line);
        processRow($row); // Handle one row, move on
    }
    fclose($handle);
}

fgets() reads one line at a time. No matter how large the file is, your memory usage stays almost flat because you're only ever holding one line in memory at a time.

For very large imports: Look into MySQL's LOAD DATA INFILE command — it can import millions of rows directly into a table far faster than PHP ever could.

Related Tutorial: PHP File Handling for Large Datasets — Memory-Efficient Techniques


Bug #30 — PDO Not Reporting Database Errors

The problem: Your database query fails and you have absolutely no idea why — no error, no exception, nothing.

// ❌ Wrong
$pdo = new PDO("mysql:host=localhost;dbname=mydb", $user, $pass);
$stmt = $pdo->query("SELECT * FROM nonexistent_table");
// Fails silently. $stmt is false. No error thrown.

By default, PDO operates in silent mode. Errors don't throw exceptions — they just return false and move on. If you're not checking every return value manually, failures disappear without a trace.

// ✅ Correct
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 it
    die("A database error occurred. Please try again.");
}

PDO::ERRMODE_EXCEPTION tells PDO to throw a PDOException whenever something goes wrong. You can catch it, log the real error message, and show the user a clean message without exposing your database internals.

Production rule: Never echo $e->getMessage() directly to the user. That message might contain your table names, column names, or connection details. Log it with error_log() and display a generic message publicly.


Quick Reference — All 10 Bugs at a Glance

Bug The Mistake The Fix
#21 strlen() on multibyte text Use 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, not extension
#25 Comma-formatted price wrong calc Strip comma, then floatval()
#26 All cart objects share same items Remove static, use instance property
#27 Code runs after redirect Add exit() after every header() redirect
#28 SQL injection via search input Use 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

Final Thoughts

The pattern you'll notice across these 10 bugs: most of them involve assuming something worked when it didn't. Whether it's assuming strlen() counts characters, assuming header() stops execution, or assuming PDO will tell you when something breaks — PHP often quietly lets you be wrong.

The fix for that is the same across all of them: be explicit. Validate your assumptions. Check return values. Set error modes. And whenever you're touching user input or external data, treat it as untrusted until proven otherwise.

If you're working through these bugs in a real project, try reproducing each one locally before applying the fix. Reading about a bug and actually watching it fail are two different experiences — the second one sticks.

Next up in the series: PHP session security, password hashing, and CSRF protection — all equally important, and all commonly done wrong.

Frequently Asked Questions (FAQs)

Q1: Why does strlen() return a higher number than the actual character count in PHP?

strlen() counts bytes, not characters. In UTF-8, non-Latin characters like Hindi or Arabic take 2–3 bytes each. A 6-character Hindi word returns 18 because each character is 3 bytes. Use mb_strlen($string, 'UTF-8') to get the correct character count.

Q2: Is checking file extension enough to validate file uploads in PHP?

No — extension checks are easy to bypass. An attacker can rename shell.php to shell.jpg and your check passes. Always verify the actual file content using finfo_file() which reads real MIME type, not the filename. Extension alone is never a reliable security check.

Q3: Does calling header("Location: ...") in PHP stop the rest of the script from running?

No. header() sends a redirect to the browser but PHP keeps executing every line after it. Always add exit() immediately after the redirect. Without it, restricted code like database deletes can still run on the server — a serious security risk.

Q4: Why is PDO not throwing any errors even when a database query fails?

PDO operates in silent mode by default — failed queries return false with no exception or warning. Set PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION when creating your connection. This makes PDO throw a catchable PDOException on any failure instead of silently doing nothing.

Q5: What is the safest way to handle search queries with user input in PHP?

Use prepared statements with PDO — never concatenate user input into SQL strings. Pass the search value as a parameter: $stmt->execute(['%' . $search . '%']). This separates data from query logic completely, preventing SQL injection regardless of what characters the user types.

Related Tags:

PHP

PHP Bugs

PHP Tutorial

Web Development

Backend Development

PHP Security

SQL Injection

PHP Debugging

PHP OOP

PHP PDO

PHP File Handling

PHP String Functions

PHP Best Practices

PHP for Beginners

Server-Side Programming

Hi, I'm Bikki Singh — Full Stack Developer, coding language trainer, and founder of CodePractice.in. With 5+ years of hands-on web development experience, I've trained 500+ students across India in Python, PHP, Java, C, C++, MySQL, and front-end technologies like HTML, CSS, and JavaScript. I started CodePractice.in with one goal: make programming education practical, not theoretical. Every tutorial and blog I write is built around real projects and interview scenarios — so learners don't just understand code, they can actually use it.

CodePractice Blog Author

Full Stack Developer, CodePractice Founder

Bikki Singh

Submit Your Reviews

Go Back Top