惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

宝玉的分享
宝玉的分享
Engineering at Meta
Engineering at Meta
让小产品的独立变现更简单 - ezindie.com
让小产品的独立变现更简单 - ezindie.com
博客园 - 聂微东
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
Last Week in AI
Last Week in AI
酷 壳 – CoolShell
酷 壳 – CoolShell
博客园 - 三生石上(FineUI控件)
T
Tailwind CSS Blog
Apple Machine Learning Research
Apple Machine Learning Research
Hugging Face - Blog
Hugging Face - Blog
爱范儿
爱范儿
博客园 - 司徒正美
人人都是产品经理
人人都是产品经理
Jina AI
Jina AI
博客园 - 叶小钗
雷峰网
雷峰网
罗磊的独立博客
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
博客园 - Franky
WordPress大学
WordPress大学
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
阮一峰的网络日志
阮一峰的网络日志
量子位

DEV Community

Authentication Security Deep Dive: From Brute Force to Salted Hashing (With Java Examples) Why AI Systems Don’t Fail — They Drift Spilling beans for how i learn for exam😁"Reinforcement Learning Cheat Sheet" I Replaced Chrome with Safari for AI Browser Automation. Here's What Broke (and What Finally Worked) How Python Borrows Other People's Work The $40 Architecture: Processing 1 Billion API Requests with 99.99% Uptime Vibe Coding: A Workflow Guide (From Zero to SaaS) Most webhook security guides protect the wrong side. The scary part is delivery. Headless CMS for TanStack Start: Build a Blog with Cosmic EU Age Verification App "Hacked in 2 Minutes" — What Actually Happened Comfy Cloud’s delete function does not actually remove files Running AI Models on GPU Cloud Servers: A Beginner Guide Event-driven media intelligence with AWS Step Functions and Bedrock I scored 500 AI prompts across 8 quality dimensions — here's what broke How to Call Google Gemini API from Next.js (Free Tier, No Backend Needed) The Portal Protocol: Reclaiming Human Connection in the Age of AI How to Fix Your Team's Scattered Knowledge Problem With a Self-Hosted Forum Intro to tc Cloud Functors: A Graph-First Mental Model for the Modern Cloud Designing Multi-Tenant Backends With Both Ownership and Team Access I Built a Neumorphic CSS Library with 77+ Components — Here's What I Learned PostgreSQL Performance Optimization: Why Connection Pooling Is Critical at Scale Cómo construí un SaaS multi-rubro para gestionar expensas en Argentina con FastAPI + Vue 3 🚀 I Built an Ethical Hacking Scanner Tool – Open Source Project I Replaced /usage and /context in Claude Code With a Single Statusline A Pythonic Way to Handle Emails (IMAP/SMTP) with Auto-Discovery and AI-Ready Design I Collected 8.9 Million Polymarket Price Points — Here's What I Found About How Markets Really Move EcoTrack AI — Carbon Footprint Tracker & Dashboard Everyone's Using AI. No One Agrees How. 5 self-hosted ebook managers worth trying in 2026 Building Your First AI Agent with LangChain: From Chatbot to Autonomous Assistant
PHP Bugs #21 to #30 — Common Mistakes Every PHP Developer...
Bikki Singh · 2026-06-19 · via DEV Community

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:

  1. file_get_contents() might have failed silently
  2. The response might not be valid JSON
  3. 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.