On 10 November 2026 the Laravel team released v12.72.0 and v13.42.0 of laravel/framework. The 12.x release has several security-related fixes. The most important one is for temporary signed URLs, which a lot of apps use for email verification, unsubscribe links, password-less logins and private downloads. This guide explains what changed, how to upgrade, and how to write tests that keep your signed links safe.
What’s in Laravel 12.72.0
The official v12.72.0 release notes list these changes:
- Fix temporary signed URL expiration bypass via numeric query-fragment parameter smuggling (#62401)
- Fallback to retrieving the
expiresquery string from theQUERY_STRINGheader (#62349) - Allow retrieving
expiresfrom$_SERVER['VAPOR_RAW_QUERY_STRING'](#62391) - Enforce that valid email addresses comply with RFC 5321 (#62347)
- Fix nested
sometimesvalidation rules being skipped when the value is__missing__(#62402) - Return each event’s own expression and timezone from
schedule:list --json(#62358)
Most of the release is about one thing: making sure Laravel reads the real expires value of a signed URL, so an attacker can’t trick it into accepting an expired link. The release notes don’t give more detail than the PR titles, and you don’t need more to act. If you’re on Laravel 12, upgrade.
What about Laravel 13?
v13.42.0 was tagged the same day. When we wrote this, its release notes and its CHANGELOG entry were empty, and the changelog jumps straight from v13.40.0 to v13.42.0. Security fixes usually land on both supported branches, but we couldn’t confirm the exact contents of 13.42.0. If you’re on 13.x, update to the latest 13.x tag and check the changelog again once the notes are filled in.
Step 1: Check Your Current Version
composer show laravel/framework | grep versions
# or
php artisan --version
Step 2: Upgrade
Both releases are minor updates within their major version, so an ordinary composer update is enough.
# Laravel 12 apps
composer update laravel/framework --with-all-dependencies
# Laravel 13 apps
composer update laravel/framework --with-all-dependencies
# Confirm
composer show laravel/framework | grep versions
php artisan optimize:clear
Commit the updated composer.lock and deploy as usual. If you use Laravel Vapor, note that PR #62391 specifically adds support for reading expires from Vapor’s raw query string, so Vapor apps get the fix too.
A Quick Refresher: How Temporary Signed URLs Work
A temporary signed URL carries a signature parameter and an expires timestamp. Laravel recomputes the signature from the URL and rejects the request if the signature doesn’t match or the timestamp has passed.
// routes/web.php
use App\Http\Controllers\UnsubscribeController;
use Illuminate\Support\Facades\Route;
Route::get('/unsubscribe/{user}', UnsubscribeController::class)
->name('unsubscribe')
->middleware('signed');
// Generating a link that is valid for 30 minutes
use Illuminate\Support\Facades\URL;
$url = URL::temporarySignedRoute(
'unsubscribe',
now()->addMinutes(30),
['user' => $user->id]
);
The signed middleware rejects invalid or expired links with a 403 response. You can also check manually inside a controller:
public function __invoke(Request $request, User $user)
{
if (! $request->hasValidSignature()) {
abort(403, 'This link is invalid or has expired.');
}
$user->update(['subscribed' => false]);
return view('unsubscribed');
}
Step 3: Add Tests That Protect Your Signed Links
The best protection against regressions is a small test suite that checks the three things that matter: valid links work, expired links fail, and tampered links fail. These tests use Laravel’s built-in time-travel helpers.
<?php
namespace Tests\Feature;
use App\Models\User;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\URL;
use Tests\TestCase;
class SignedUnsubscribeLinkTest extends TestCase
{
use RefreshDatabase;
public function test_valid_link_works(): void
{
$user = User::factory()->create();
$url = URL::temporarySignedRoute('unsubscribe', now()->addMinutes(30), ['user' => $user->id]);
$this->get($url)->assertOk();
}
public function test_expired_link_is_rejected(): void
{
$user = User::factory()->create();
$url = URL::temporarySignedRoute('unsubscribe', now()->addMinutes(30), ['user' => $user->id]);
$this->travel(31)->minutes();
$this->get($url)->assertForbidden();
}
public function test_tampered_link_is_rejected(): void
{
$user = User::factory()->create();
$other = User::factory()->create();
$url = URL::temporarySignedRoute('unsubscribe', now()->addMinutes(30), ['user' => $user->id]);
$tampered = str_replace("/unsubscribe/{$user->id}", "/unsubscribe/{$other->id}", $url);
$this->get($tampered)->assertForbidden();
}
}
Run them with php artisan test --filter=SignedUnsubscribeLinkTest. Keep them in CI so every future framework update gets checked automatically.
Step 4: Re-Check Email Validation
PR #62347 makes Laravel’s email validation enforce RFC 5321. That means some addresses your app accepted before may now fail validation. Before you deploy, run your existing tests and try a few real-world addresses from your own database against your rules:
use Illuminate\Support\Facades\Validator;
$samples = ['[email protected]', '[email protected]'];
foreach ($samples as $email) {
$v = Validator::make(['email' => $email], ['email' => 'required|email']);
echo $email . ' => ' . ($v->passes() ? 'ok' : 'rejected') . PHP_EOL;
}
If a sign-up form starts rejecting addresses after the update, this script shows you which rule is responsible.
Step 5: Re-Check Nested sometimes Rules
PR #62402 fixes nested sometimes rules that were being skipped when the value was missing. If you validate nested payloads like this, test with fields both present and absent:
$request->validate([
'shipping' => 'sometimes|array',
'shipping.postcode' => 'sometimes|required|string|max:10',
]);
After the fix, rules like these may run in cases where they were silently skipped before. That’s correct behaviour, but it can expose bad data your API was quietly accepting.
Upgrade Checklist
- Update
laravel/frameworkto 12.72.0+ (12.x) or the latest 13.x tag. - Run your test suite, especially email validation and nested validation tests.
- Add the three signed-URL tests above if you don’t have them yet.
- Deploy, then run
php artisan optimize:clear(or your usual cache rebuild). - Check the 13.x changelog again once the v13.42.0 notes are published.
Takeaway
Signed URLs protect sensitive actions, so an expiration bypass is worth fixing today. On Laravel 12, update to 12.72.0. On Laravel 13, move to the newest 13.x release. Either way, add a few time-travel tests so you’ll know straight away if signed links ever break again. Sources: laravel/framework releases.
