Security Best Practices

Laravel protects against most common web vulnerabilities by default, without any extra setup — but "by default" is not the same as "no matter what you do." Several of these protections can be accidentally switched off, and it's worth understanding exactly what Laravel is doing for you so you recognize the moment you're about to undo it.

CSRF Protection: The @csrf Directive

Cross-Site Request Forgery protection stops a malicious third-party site from silently submitting a form to your application using a logged-in user's own browser session. Laravel's VerifyCsrfToken middleware checks every state-changing request (POST, PUT, PATCH, DELETE) for a valid token — which means every Blade form needs the @csrf directive inside it:

<form method="POST" action="/articles">
    @csrf
    <input type="text" name="title">
    <button type="submit">Save</button>
</form>

@csrf renders a hidden input containing a token tied to the user's session. Without it, submitting the form doesn't silently succeed — Laravel rejects the request outright with a 419 "Page Expired" response, which is usually the first clue that a form is missing its @csrf field.

Mass Assignment Protection: $fillable vs. $guarded

As covered in Eloquent ORM Basics, calling Article::create($request->all()) mass-assigns every key in the request straight onto model attributes. $fillable is the whitelist that makes this safe, by naming exactly which columns are allowed to be set this way:

<?php
class Article extends Model
{
    protected $fillable = ['title', 'body'];
}
Why $guarded = [] is dangerous: $guarded is the inverse — it names columns that are not mass-assignable, and everything else is allowed. Setting protected $guarded = []; means nothing is protected — every column on the table, including ones like is_admin or role that a form should never be able to touch, becomes settable from raw request input. It's tempting because it "just works" during development, but it quietly removes a real security boundary.

SQL Injection: Automatic Protection, and How to Break It

Every query built through Eloquent or the Query Builder is automatically parameter-bound — values are never concatenated directly into the SQL string, so user input can't be crafted to change the query's meaning. That protection covers where(), find(), and everything else built the normal way, with no extra effort required.

That automatic protection stops, however, the moment you build raw SQL yourself by concatenating a string:

Dangerous — user input concatenated directly into raw SQL:
<?php
$users = DB::select("select * from users where email = '" . $request->email . "'");
A malicious value for email can break out of the quotes and change what the query does entirely — this is exactly the class of bug SQL injection describes.

The fix is to keep using parameter binding, even in raw SQL — a ? placeholder with values passed separately:

<?php
$users = DB::select('select * from users where id = ?', [$id]);

With a placeholder, the database driver sends the value separately from the query structure, so there's no way for it to be interpreted as SQL — no matter what characters it contains.

XSS Protection: Blade's Automatic Escaping

As covered in Blade Templating Basics, Blade's standard {{ }} output syntax runs every value through PHP's htmlspecialchars() automatically, converting characters like < and > into harmless entities before they reach the page:

<p>{{ $comment->body }}</p> <!-- safe: any HTML/JS in $comment->body is escaped, not executed -->
The danger of {!! !!}: Blade's raw output syntax, {!! !!}, skips escaping entirely and prints the value exactly as stored — appropriate only for content you trust completely, like HTML you generated yourself. Used on untrusted user input, it's a direct Cross-Site Scripting hole: a comment body containing <script> tags would execute in every other visitor's browser that views the page.
<p>{!! $comment->body !!}</p> <!-- dangerous if $comment->body came from user input -->

Password Hashing With the Hash Facade

Passwords should never be stored as plaintext, and Laravel makes the correct behavior the easy one — the Hash facade wraps a strong, salted hashing algorithm (bcrypt by default) behind a single call:

<?php
use Illuminate\Support\Facades\Hash;

$user->password = Hash::make($request->password);
$user->save();

Checking a login attempt works the same way, comparing the hash rather than the raw string: Hash::check($request->password, $user->password). Never write your own hashing scheme or store a password with a reversible encryption — Hash::make() exists specifically so you never have to.

Rate Limiting Sensitive Routes

Login forms, password-reset requests, and similar routes are prime targets for brute-force attempts — hundreds of guesses fired at once. The throttle middleware, covered in Middleware, caps how many requests a given user or IP can make in a time window:

<?php
Route::post('/login', [LoginController::class, 'login'])
        ->middleware('throttle:5,1');

throttle:5,1 allows at most 5 attempts per minute from the same source before returning a 429 Too Many Requests response — enough friction to make automated password guessing impractical, without getting in a real user's way.

Keeping Dependencies Updated

Laravel and every package it depends on are installed and versioned through Composer, and security fixes are published regularly. Checking for outdated packages, and updating deliberately rather than letting a project drift for years, is a real security practice, not just housekeeping:

# see which installed packages have newer versions available
composer outdated

# update dependencies according to composer.json version constraints
composer update
Last lesson: With the application itself hardened, the final page of this track pulls everything covered so far into one checklist — head to Best Practices.