Week 25: Cross-Stack PHP Security Deep Dive

Weeks 8, 13 and 24 each covered security within one stack. This week steps back and compares all three side by side — the same three vulnerability classes, the same root causes, expressed through three different sets of framework tools — plus dependency auditing, a concern that applies identically no matter which stack a project uses.

Module 22 of 28 Week 25 of 32 ~3–4 Hours Hands-on Exercise Included

By the end of this week, you'll be able to

  • Compare CSRF, XSS & SQL injection defenses across raw PHP, Laravel & WordPress
  • Configure sessions & cookies securely in each stack
  • Audit a project's dependencies for known vulnerabilities

1. CSRF, Compared

Same underlying vulnerability from Week 8, three different implementations of the identical fix — a per-session (or per-action) token an attacker's page can't know:

raw PHP (Week 8)
<?php
$_SESSION['csrf_token'] ??= bin2hex(random_bytes(32));
// ... embed as a hidden field, verify with hash_equals() on submit
Laravel — automatic on every web route
<form method="POST" action="/tasks">
  @csrf
  <!-- expands to a hidden _token field; VerifyCsrfToken middleware checks it automatically -->
</form>
WordPress — nonces (Week 24)
<?php wp_nonce_field('pm_save_inquiry', 'pm_nonce'); ?>
<!-- verified manually with wp_verify_nonce() -->

The pattern across all three: generate an unpredictable, scoped token, embed it in the form, verify it on submission before trusting the request. Laravel is alone in applying this automatically to every web route by default — raw PHP and WordPress both require deliberately opting in per form.

2. XSS, Compared

Same rule everywhere — escape dynamic content at the point of output, matched to the exact context — expressed through different default behaviors:

raw PHP — manual, every time (Week 1, 8)
<?= htmlspecialchars($comment['body']) ?>
Laravel Blade — automatic by default (Week 9)
{{ $task->title }}
<!-- escaped automatically; {!! !!} opts OUT of escaping, rarely used -->
WordPress — manual, context-specific (Week 19, 24)
<?= esc_html($client) ?>   <!-- text content -->
<?= esc_attr($client) ?>   <!-- attribute value -->

Blade's "escaped by default, opt out deliberately" design is objectively harder to get wrong than raw PHP's or WordPress's "escape manually, every single time" — worth naming directly as a real trade-off in framework design: Blade trades a small amount of flexibility (an unusual syntax for the rare unescaped case) for eliminating an entire class of easy-to-forget mistakes.

3. SQL Injection, Compared

Every stack in this course ultimately resolves to the same underlying fix — structural separation of SQL from data — through a different-looking API:

raw PDO (Week 7)
$stmt = $pdo->prepare('SELECT * FROM customers WHERE email = :email');
$stmt->execute(['email' => $email]);
Eloquent (Week 11-12) — prepared statements underneath
$customer = Customer::where('email', $email)->first();
// Compiles to the exact same kind of prepared statement, generated for you
$wpdb (Week 21)
$wpdb->get_results($wpdb->prepare('SELECT * FROM customers WHERE email = %s', $email));

None of these are "safer SQL injection prevention" than the others — they're the same mechanism wearing three different syntaxes. Eloquent's query builder makes it genuinely hard to accidentally concatenate raw input into a query, which is a real safety advantage in practice, but the underlying guarantee (bound parameters, never interpreted as SQL syntax) is identical across all three.

Every stack still has an "escape hatch" that reintroduces the risk

Eloquent's DB::raw(), Laravel's DB::select() with concatenated strings, and $wpdb->query() called without prepare() all bypass the safety guarantee entirely. Framework defaults help, but the underlying discipline from Week 7 — never concatenate untrusted input into SQL — still has to hold at every call site, including the "raw" escape hatches every framework provides.

4. Secure Session & Cookie Configuration

Beyond the mechanics from Week 5, a few php.ini/framework settings matter specifically for production:

php.ini — hardened session settings
session.cookie_httponly = 1  ; inaccessible to JavaScript -- blocks cookie theft via XSS
session.cookie_secure = 1    ; only sent over HTTPS
session.cookie_samesite = "Lax" ; blocks the cookie being sent on most cross-site requests
session.use_strict_mode = 1  ; rejects uninitialized session IDs -- helps prevent fixation

SameSite=Lax is directly relevant to the CSRF discussion above — it's a second, independent layer of defense (the browser itself refusing to attach the cookie to most cross-site requests) on top of the CSRF token mechanism, not a replacement for it. Laravel's config/session.php exposes the same settings; WordPress relies on the underlying PHP session configuration by default.

5. Auditing Dependencies

Every Composer package (Week 4) and WordPress plugin (Weeks 20-24) is code you didn't write, running with the same privileges as code you did. Known vulnerabilities in dependencies are a genuinely common real-world breach vector:

terminal — Composer projects
composer audit
# Reports known CVEs in your installed dependencies, cross-referenced
# against their exact locked versions in composer.lock
terminal — WordPress plugins
wp plugin list --update=available
# WordPress core also flags known-vulnerable plugins directly in /wp-admin

Running composer audit (or WordPress's own update notifications) as a routine part of maintenance — not just once at project setup — matters because new CVEs are discovered in existing, unchanged packages constantly; a dependency that was safe last month isn't guaranteed to still be safe today.

6. Hands-on Exercise

Hands-on

Security-audit all three projects from this course

Apply this week's cross-stack comparison as a real checklist against your own code.

Requirements:

  1. Run composer audit against both the Module 1 CRUD app and the Laravel task tracker, and resolve or document any findings.
  2. In the Laravel app, confirm session cookies are configured with secure, http_only and same_site => 'lax' in config/session.php.
  3. Grep the raw PHP task manager (Week 8) for every echo/<?= ?> printing dynamic content, and confirm every single one is wrapped in htmlspecialchars().
  4. Grep the WordPress plugin for every form/AJAX handler and confirm each verifies a properly scoped nonce before any write.
  5. Write a short summary (a paragraph per project) of what you found and fixed.
Hint

This kind of systematic audit — grepping for every output point and confirming each is escaped, rather than trusting memory — is exactly the discipline a professional security review applies. Doing it once yourself on real code makes the underlying pattern much easier to recognize automatically going forward.

7. Knowledge Check

Four quick questions. Expand each to check your answer.

Q1

Is Laravel's automatic CSRF protection a fundamentally different mechanism from Week 8's hand-built version, or the same idea automated?

The same idea, automated. Both generate an unpredictable, session-scoped token, embed it in the form, and verify it before trusting a state-changing request. Laravel's advantage isn't a different mechanism — it's that VerifyCsrfToken middleware applies this automatically to every web route by default, rather than requiring a developer to remember to add it manually to each form, the way raw PHP and WordPress both do.

Q2

Why is Blade's "escape by default, opt out with {!! !!}" design considered safer in practice than raw PHP's or WordPress's "escape manually every time"?

Forgetting to escape a value is an easy, common mistake when escaping is something a developer must remember to add at every single output point. Making the safe behavior the default and requiring a deliberate, visually distinct opt-out for the unsafe case ({!! !!}) shifts the failure mode: instead of a forgotten htmlspecialchars() call silently introducing an XSS vulnerability, a genuinely intentional unescaped output stands out clearly in the code.

Q3

Does using Eloquent instead of raw PDO make SQL injection structurally impossible?

No. Eloquent's normal query builder methods compile to prepared statements exactly like raw PDO, but every framework in this course provides an "escape hatch" (DB::raw(), unprepared DB::select() calls, $wpdb->query() without prepare()) that reintroduces the exact same risk if untrusted input is concatenated into it. The underlying discipline from Week 7 still has to hold everywhere, including inside those raw escape hatches.

Q4

Why run composer audit repeatedly over a project's lifetime, rather than just once when dependencies are first installed?

New vulnerabilities (CVEs) are discovered in existing, already-installed package versions on an ongoing basis — a dependency that had no known issues when it was first installed can later be found to have a serious vulnerability, with no code change on your end at all. Auditing needs to be a routine, recurring part of maintaining a project, not a one-time setup checkbox.