Skip to content

Tier 2b · Chapter 6: Security & Validation

PHP/Laravel Track · ~30 min read + exercise

A note on sources: This chapter draws mainly on general security practice and Laravel's documentation — the course's book set doesn't cover security in depth. The principles (defense in depth, least privilege, validate-at-the-boundary) are well-established industry practice; the Laravel mechanics (policies, gates, form requests, the framework's built-in protections) come from Laravel's docs. Treat tool and feature names as current-framework examples.


Why this chapter exists

Security was a quality attribute in Foundations Chapter 1 — the adversarial one, where "good enough for now" rots into "breached next quarter," and which trades against usability and performance. This chapter makes it concrete in Laravel: validating untrusted input at the boundary, authenticating who a user is, authorizing what they may do, and using the framework's built-in protections rather than rolling your own. Laravel gives you strong security tools, and the most common Laravel vulnerabilities come not from the framework's weaknesses but from developers bypassing or misusing those tools — so this chapter is as much about using what's there as about understanding the principles.

The throughline: security is layered (defense in depth) and boundary-driven — validate all input at the edge, authenticate and authorize as distinct steps, and lean on the framework's protections instead of reinventing them. The recurring architectural idea is the trust boundary from the JS/TS track and Foundations Chapter 1: untrusted input (and untrusted actors) must be checked at the perimeter, and the checks must be layered so that one missed check isn't a total breach. The Laravel-specific lesson is that the framework already implements most of the hard parts correctly, and your job is mostly to not undermine them.

What this chapter covers

  • Validation as the input trust boundary — every request validated at the edge.
  • Authentication vs. authorization — the two distinct questions, and why conflating them is dangerous.
  • Authorization with policies and gates — Laravel's mechanism for "may this user do this?"
  • Defense in depth and the framework's protections — layering, and the built-ins not to bypass.

Validation as the input trust boundary

Every request is untrusted, and validation is the boundary where untrusted input becomes trusted — the exact trust-boundary principle from the JS/TS track (parse, don't trust) and Foundations Chapter 1, in Laravel form. Skipping or weakening validation isn't just a data-quality problem; it's the entry point for a large share of security vulnerabilities, because unchecked input is what attackers manipulate.

Laravel's primary tool is the Form Request — a class that defines validation rules for a request, validated automatically before the controller runs, so the controller receives only valid input:

php
class CreateTicketRequest extends FormRequest
{
    public function rules(): array
    {
        return [
            'customer_id' => ['required', 'uuid', 'exists:customers,id'],
            'subject'     => ['required', 'string', 'max:200'],
            'priority'    => ['required', 'in:low,normal,high'],
        ];
    }
}

The architectural points beyond the mechanics:

Validate the entire input surface and validate strictly. Validate every field you accept, with rules as tight as the domain allows — an allowlist of acceptable values (in:low,normal,high) rather than accepting anything and hoping. Strict validation at the boundary is the input-security floor; loose or partial validation is where malformed and malicious input slips through. This is the foundations' "validate at the edge, trust the interior" — the controller and domain downstream should receive only input that already passed the perimeter.

Validation is necessary but not sufficient for injection defense. Validating that input is well-formed doesn't by itself prevent injection attacks — but Laravel's other layers do, if you use them. Eloquent and the query builder use parameterized queries, which prevent SQL injection by construction — the danger is dropping to raw queries with interpolated input (DB::raw("... {$userInput}")), which reintroduces the vulnerability the framework was preventing. Blade templating escapes output by default, preventing XSS — the danger is the unescaped {!! !!} syntax on user-controlled data. The pattern is consistent: the framework's safe path is safe; the vulnerabilities come from leaving it.

Validation messages shouldn't leak internals. Per Foundations Chapter 1's security/usability balance and the JS/TS API chapter's "don't leak internals," validation errors should tell the user what to fix without revealing system internals. Laravel's defaults are reasonable here; the caution is custom error handling that exposes too much.


Authentication vs. authorization

The single most important conceptual distinction in application security, and one that's dangerous to blur: authentication is who are you? — verifying identity (login, tokens, sessions). Authorization is what are you allowed to do? — deciding whether an authenticated user may perform a specific action on a specific resource. They are different questions, answered by different mechanisms, and conflating them produces a classic, severe vulnerability class.

The dangerous conflation is assuming that authenticated means authorized — that because a user is logged in, they may do whatever the request asks. This produces the insecure direct object reference: a logged-in user requests /tickets/123, the code authenticates them (they're logged in, fine) and returns ticket 123 — without checking whether ticket 123 belongs to them. Now any authenticated user can read any ticket by changing the ID. The authentication succeeded; the authorization was never done. This is one of the most common real-world vulnerabilities, and it comes precisely from treating "is logged in" as sufficient when the question that mattered was "may this user access this resource."

The architectural discipline: authenticate once at the perimeter, but authorize every action on every resource. Authentication establishes identity (Laravel's auth system, Sanctum/Passport for APIs, handles this — and the guidance is to use it, not roll your own, because authentication is easy to get subtly and catastrophically wrong). Authorization is then a separate, per-action check: for this identified user, may they do this to this thing? Every action that touches a resource a user shouldn't universally access needs its own authorization check — which is what policies are for.


Authorization with policies and gates

Laravel's mechanism for authorization is policies (and gates for simpler cases), and using them systematically is how you avoid the insecure-direct-object-reference trap. A policy is a class that answers authorization questions about a particular model — for Ticket, "may this user view this ticket? update it? resolve it?":

php
class TicketPolicy
{
    public function view(User $user, Ticket $ticket): bool
    {
        // The authorization rule: a user may view their own tickets, or any if staff.
        return $user->id === $ticket->customer_id || $user->isStaff();
    }
}

// In the controller — authorize the action on the specific resource.
public function show(Ticket $ticket)
{
    $this->authorize('view', $ticket);  // throws 403 if the policy denies
    return new TicketResource($ticket);
}

(Flagged: on Laravel 11+ the base controller no longer pulls in the AuthorizesRequests trait, so $this->authorize() isn't available out of the box — the controller must use AuthorizesRequests; explicitly, or you reach for Gate::authorize() or a can middleware on the route instead. The authorization concept is the same whichever you pick.)

The architectural value of centralizing authorization in policies:

The rules live in one findable place per resource, not scattered across controllers as ad-hoc if checks that are easy to forget on a new endpoint. A new action on tickets prompts the question "what's the policy?" rather than reinventing the check or omitting it. This is the foundations' governance instinct — putting a concern in one enforced place rather than relying on every developer to remember it everywhere.

Authorization becomes consistent and auditable. Because the rules are in the policy, you can read and reason about "who can do what to a ticket" in one place, and a security review has a single surface to audit rather than hunting through every controller. Scattered authorization is unauditable; centralized authorization is reviewable.

The check is explicit and unmissable per action. $this->authorize('view', $ticket) makes the authorization a visible, required line — and the discipline (which a code-review checklist or even a fitness-function-style scan can enforce, per Foundations Chapter 6) is that every resource-touching action has one. The insecure-direct-object-reference vulnerability is precisely a missing authorize call; making the call a consistent, expected part of every such action is how you stop it systematically rather than hoping nobody forgets.


Defense in depth and the framework's protections

Security architecture's organizing principle, echoing the data-consistency chapter's "database is the last line of defense," is defense in depth: layer multiple independent protections so that one failure isn't a total breach. No single check is assumed perfect; the layers back each other up. Applied across a Laravel app:

Layer the checks. Validation at the boundary (well-formed input), authentication at the perimeter (known identity), authorization per action (permitted action), and database constraints as the final backstop (the consistency chapter's point — a unique constraint or foreign key holds even if an application check is bypassed). A request passes through all of them; a gap in one is caught by another. An attacker who slips past one layer still faces the next.

Apply least privilege. Grant the minimum access necessary — users see only their own data, service accounts have only the permissions they need, tokens are scoped narrowly. The blast radius of any compromise is bounded by what the compromised actor was permitted, so minimizing permissions minimizes damage. This is an architectural posture, not a single feature.

Use the framework's built-in protections — don't bypass or reinvent them. This is the most practical Laravel security lesson, because most Laravel vulnerabilities are self-inflicted by leaving the safe path. Laravel ships with CSRF protection on web forms (don't disable it without understanding why), parameterized queries via Eloquent (don't hand-build SQL with user input), output escaping in Blade (don't {!! !!} user content), password hashing (don't store plaintext or roll your own hashing), and encryption helpers (don't invent your own crypto — the cardinal rule of applied cryptography is don't roll your own). The framework's implementations are reviewed, maintained, and correct; your reinvention almost certainly isn't. The security win is largely in using what's there correctly.

Keep dependencies patched. Foundations Chapter 6's fitness-function example was scanning for vulnerable dependency versions — the kind of automated check that catches a known vulnerability before it's exploited. A Laravel app's security includes keeping the framework and packages updated and scanning for known-vulnerable versions (Composer audit, automated dependency monitoring), because a patched vulnerability you didn't apply is an open door.

The recurring lesson: security in Laravel is mostly layering the framework's strong tools correctly — validate every input at the boundary, authenticate identity and authorize every action as separate steps, back the application checks with database constraints, apply least privilege, and stay on the framework's safe paths rather than reinventing them. The architecture is defense in depth; the Laravel specifics are mostly about not undermining protections you already have.


Practice

Budget about ninety minutes.

Exercise 1: Hunt for missing authorization (20 min)

In a Laravel app you work on, find endpoints that load a resource by ID. For each, check: is there an authorization check that the current user may access that specific resource, or only an authentication check that they're logged in? Every endpoint with only authentication is a potential insecure-direct-object-reference — list them.

Exercise 2: Add a policy (35 min)

Take one of those endpoints and add proper authorization: write a policy for the resource expressing who may do what, and call authorize in the controller. Test that another user is correctly denied (403). Note how the rule is now centralized and how a new action on the same resource would prompt the authorization question.

Exercise 3: Audit the safe paths (35 min)

Scan for places the app leaves the framework's safe paths: raw SQL with interpolated input, {!! !!} on user content, disabled CSRF, custom crypto or hashing, validation gaps on accepted input. For one finding, return it to the safe path and note the vulnerability it removes. Check whether dependencies are scanned for known vulnerabilities; if not, that's a gap.

Deliverable

A short security assessment for one feature: every resource-touching action's authorization status (authenticated-only vs. properly authorized), one policy added to close an insecure-direct-object-reference, and one "left the safe path" finding returned to the framework's protection — each with the vulnerability it addresses, framed as defense-in-depth layers.

Check yourself

  • Explain why validation is the input trust boundary and why it's necessary but not sufficient for injection defense.
  • Distinguish authentication from authorization and explain the insecure-direct-object-reference that comes from conflating them.
  • Explain how policies centralize authorization and why that prevents the missing-check vulnerability systematically.
  • Explain defense in depth and least privilege, and why using the framework's protections beats reinventing them.

What a strong answer demonstrates: you keep authentication (who are you) and authorization (what may you do) as genuinely separate questions and can trace the insecure-direct-object-reference precisely to treating "is logged in" as sufficient — then explain why centralizing the check in a policy stops it systematically (one findable, auditable place) rather than hoping nobody forgets an if. The strongest answers frame the whole chapter as defense in depth — validation, authentication, per-action authorization, and database constraints backing each other up — and treat most Laravel vulnerabilities as self-inflicted by leaving the framework's safe path, not framework weakness.

Going deeper (optional)

(This chapter is general security practice plus Laravel docs; the course book set doesn't cover security in depth.)

  • Primary source: the OWASP Top Ten (current edition: OWASP Top 10:2021, with a 2025 revision now published) is the standard, authoritative reference for the vulnerability classes here — A01 Broken Access Control is exactly the insecure-direct-object-reference this chapter centers on, and A03 Injection the parameterized-query point.
  • Primary source: Jerome Saltzer & Michael Schroeder, The Protection of Information in Computer Systems (Proceedings of the IEEE, 1975) — the seminal paper that named the design principles this chapter rests on: least privilege, complete mediation (every access to every object checked — the formal root of "authorize every action on every resource"), and fail-safe defaults.
  • The Laravel documentation on authentication, authorization (policies/gates), validation, and the security features (CSRF, encryption, hashing) is the source for the framework mechanics.
  • Foundations Chapter 1 (security as a quality attribute) and Chapter 6 (dependency-scanning fitness functions) connect it back to the course.

Key takeaways

  • Security is layered (defense in depth) and boundary-driven — validate all input at the edge, and layer protections so one gap isn't a breach.
  • Authentication (who are you) and authorization (what may you do) are distinct — assuming authenticated means authorized produces the insecure-direct-object-reference, a top real-world vulnerability.
  • Authorize every action on every resource via centralized policies, so the rules are findable, auditable, and the check is unmissable — versus scattered ifs that get forgotten.
  • Use the framework's protections, don't reinvent them: Eloquent's parameterized queries, Blade escaping, CSRF, built-in hashing and crypto are correct; vulnerabilities mostly come from leaving the safe path. Apply least privilege and keep dependencies patched.

Next: Testing in Laravel — feature tests, unit tests, and architectural boundary checks, where the testability the earlier chapters built becomes verification.