3.4Security: SQL injection, XSS, CSRF, hashing
The essentials in 30 seconds
- Never trust inputs: all outside data is suspect — validate, then handle per context (SQL, HTML, email).
- Three flaws, three reflexes: SQL injection → prepared statements; XSS → htmlspecialchars() on output; CSRF → verified unique token + SameSite.
- Passwords never in plain text: password_hash() when storing, password_verify() when logging in — no password hardcoded in code.
Running case — Riad audits his shop before going live
Riad, web developer in Constantine, is about to put his shop online: comments displayed raw, order form with no token, client passwords in plain text in the database. He fixes everything: htmlspecialchars() on each output, a unique CSRF token per form verified on submission, password_hash() at registration and password_verify() at login. The audit goes green before opening.
Last episode of the Riad thread — from database to objects to full armouring.
1. SQL injection, XSS, CSRF: mechanisms and defences
Security is permanent, not a final option, around one principle: NEVER trust inputs. (1) SQL injection (seen in 3.1): SQL injected via concatenated data — defence: PDO prepared statements, always. (2) XSS: JavaScript injected that runs in someone else's browser when untrusted data is displayed unescaped (session theft, defacement) — defence: escape on output with htmlspecialchars(), always, for any untrusted data.
(3) CSRF: forcing a logged-in user to perform an action unknowingly via a forged request from another site — defence: CSRF tokens (unique secret value per form, verified on submission) and SameSite cookies. Riad adds a token generated with random_bytes() as a hidden field in each sensitive form and compares it in session before any processing.
2. Hashing, HTTPS, secrets: non-negotiable hygiene
Passwords are stored HASHED, never in plain text: password_hash() at registration (safe algorithm, built-in salt and cost), password_verify() at login. No password in plain text or hardcoded in code, screenshots or messages — non-negotiable.
The rest of hygiene: HTTPS in production (encrypt), secrets out of code (database keys and passwords in environment variables), detailed errors never displayed in production (logged, not disclosed), PHP and dependencies up to date, authorization checked (not just authentication). To learn: the OWASP Top 10, the web-flaw reference.
🔎 Concrete example — XSS neutralized, CSRF verified, password hashed
Riad secures a pseudo display, a form and registration.
Hello, ' . htmlspecialchars($pseudo, ENT_QUOTES, 'UTF-8') . '';
// 2) CSRF: unique token in session, hidden field, verification
if (empty($_SESSION['csrf'])) {
$_SESSION['csrf'] = bin2hex(random_bytes(32));
}
// in the form:
if (!hash_equals($_SESSION['csrf'], $_POST['csrf'] ?? '')) {
http_response_code(403);
exit('Request denied.');
}
// 3) Hashing: never in plain text
$hash = password_hash($motDePasseSaisi, PASSWORD_DEFAULT);
// ... storing the $hash in the database via prepared query ...
$ok = password_verify($motDePasseSaisi, $hashStocke);
Reading: escaping on output, token compared with hash_equals, hashing then verification.
Lesson: three short moves closing three serious flaws.
Section vocabulary
- Never trust inputs
- Cross-cutting principle: all outside data is potentially malicious — validate then handle per context.
Simple example: Riad validates the email before any use.
What it is not: displaying as-is “because it comes from my form”.
Reflex: suspect by default, validated before use.
- SQL injection → prepared
- SQL injected via concatenation; defence: always PDO prepared statements, never concatenation.
Simple example: Riad's shop prepare/execute.
What it is not: a “temporary” concatenation.
Reflex: placeholders + execute, no exceptions.
- XSS → htmlspecialchars
- Injected script executed in other visitors' browsers; defence: escape all untrusted data on output.
Simple example: The commented pseudo displayed as harmless text.
What it is not: a direct echo of a user field.
Reflex: htmlspecialchars() on every untrusted output.
- CSRF → tokens
- Action forced unknowingly on a logged-in user; defence: unique per-form token verified + SameSite cookie.
Simple example: The hidden field compared in session at Riad's.
What it is not: a sensitive form with no token at all.
Reflex: a token on every sensitive action.
- Hashing / HTTPS / secrets
- Hashed passwords (password_hash/verify, never plain text); HTTPS; environment secrets; errors hidden in prod; up to date.
Simple example: Riad's hashed registration and verified login.
What it is not: a password stored readably in the database.
Reflex: no secret in plain text, anywhere.
In practice — Securing a PHP application
- Against SQL injection: ALWAYS prepared statements (PDO) — check no data is concatenated into SQL.
- Against XSS: escape all displayed data with htmlspecialchars() (comments, pseudos, database data).
- Against CSRF: add a CSRF token (unique, secret) to your forms and verify it on submission; use SameSite on cookies.
- Hash passwords (password_hash/verify), use HTTPS, manage secrets out of code, do not display errors in production, keep PHP/dependencies up to date.
Ready-to-say script — presenting your security audit (30 s)
“Before going live: prepared statements everywhere, htmlspecialchars on every output, verified CSRF token on sensitive forms, passwords hashed with password_hash and verified with password_verify. Secrets in environment, HTTPS in production.”
Say in pre-launch review; show the four moves in the code.
Copy-paste template — safe registration and login
prepare('INSERT INTO utilisateurs (email, mot_de_passe) VALUES (:email, :hash)');
$stmt->execute(['email' => $email, 'hash' => $hash]);
// Login: read with prepare, verify the hash
$stmt = $pdo->prepare('SELECT id, mot_de_passe FROM utilisateurs WHERE email = :email');
$stmt->execute(['email' => $emailSaisi]);
$utilisateur = $stmt->fetch();
if ($utilisateur && password_verify($motDePasseSaisi, $utilisateur['mot_de_passe'])) {
session_regenerate_id(true);
$_SESSION['utilisateur_id'] = $utilisateur['id'];
}
// Output: always escape
$pseudoAffiche = htmlspecialchars($pseudo, ENT_QUOTES, 'UTF-8');
Independent
Prepared + htmlspecialchars + hashing: the trio is enough to launch with no serious fault.
SME in Constantine
Riad adds CSRF tokens to order slips before opening.
Product team
Enforce the four-move checklist in systematic code review.
Beginner
This week: one escaped output and one hashed registration.
✓ Best practices
- Think security from design, at every layer (input, SQL, HTML, action, storage).
- Use proven tools (prepared, htmlspecialchars, password_hash, tokens) rather than in-house.
- Keep learning (OWASP Top 10): attacks evolve.
✗ Common pitfalls
- Displaying data from “your own database” unescaped (it comes from users).
- Forgetting the CSRF token on a sensitive action (order, deletion).
- Displaying detailed errors in production (disclosed information).
Key takeaways
- SECURITY is PERMANENT, around one principle: NEVER TRUST inputs (all outside is suspect).
- SQL INJECTION → PREPARED (never concatenation). XSS → ESCAPE with htmlspecialchars(). CSRF → TOKENS + SameSite.
- HASHED passwords (password_hash/verify, NEVER plain text or hardcoded). HTTPS, SECRETS out of code, errors hidden in prod, UP TO DATE.
- PROVEN tools + training (OWASP Top 10). An unsecured site endangers its users — professional responsibility.
48h challenge — apply tomorrow
Add 1 verified CSRF token to a form, escape 3 outputs and convert 1 storage to password_hash.
- I recite the four defences without hesitation.
- No data is concatenated or displayed raw.
- No password exists in plain text in my code.
Frequently asked questions
XSS and CSRF: concretely, how to protect against them?
For XSS: an attacker slips JavaScript into a field (comment, pseudo) displayed unescaped — it runs in visitors' browsers (session theft, redirection). Defence: htmlspecialchars() on all untrusted data before output (the script becomes harmless text). For CSRF: a forged request from another site makes a logged-in user act unknowingly (changing an email, ordering). Defence: unique secret token per form, verified on submission (+ SameSite on cookies). Two different mechanisms, two systematic defences.
Where to start with security, and which mindset to adopt?
The mindset: “never trust inputs” — all outside is suspect (even “your” UI, bypassable with direct requests). High-impact fundamentals: (1) SQL injection → prepared, (2) XSS → escape, (3) CSRF → tokens, (4) passwords → hashing, (5) HTTPS, (6) secrets out of code, (7) errors hidden in prod, (8) authorization (rights, not just login), (9) locked-down uploads. Then the OWASP Top 10 to structure what follows.
A visitor posts a pseudo with JavaScript in it. Correct reflex?