From fd4e4623c67f4514dfaca5c8640f4fa345c8c1d3 Mon Sep 17 00:00:00 2001 From: Brett B Date: Tue, 23 Jun 2026 11:36:09 +1000 Subject: [PATCH] Parse URLs more effectively to allow tel: links and prevent ftp: and file: --- src/Middleware/ValidateSignature.php | 13 +++++++---- tests/MailTrackerTest.php | 34 ++++++++++++++++++++++++++-- 2 files changed, 41 insertions(+), 6 deletions(-) diff --git a/src/Middleware/ValidateSignature.php b/src/Middleware/ValidateSignature.php index 9c8a695..85172ee 100644 --- a/src/Middleware/ValidateSignature.php +++ b/src/Middleware/ValidateSignature.php @@ -5,6 +5,7 @@ use Closure; use Illuminate\Routing\Middleware\ValidateSignature as Middleware; use Illuminate\Support\Facades\Event; +use Illuminate\Support\Uri; use jdavidbakr\MailTracker\Events\ValidLinkEvent; use jdavidbakr\MailTracker\Exceptions\BadUrlLink; use jdavidbakr\MailTracker\MailTracker; @@ -14,17 +15,21 @@ class ValidateSignature extends Middleware { public function handle($request, Closure $next, ...$args) { - $hash = $request->get('h'); - $url = $request->get('l'); + $hash = $request->query('h'); + $url = $request->query('l'); - if (filter_var($url, FILTER_VALIDATE_URL) === false) { + $uri = Uri::of($url); + + $validUriSchemes = ['http', 'https', 'tel', 'mailto']; + + if (!in_array($uri->scheme(), $validUriSchemes)) { throw new BadUrlLink('Mail hash: ' . $hash . ', URL: ' . $url); } [$relative, $ignore] = $this->parseArguments($args); // If the signature is valid then we know that it has not been tampered with so continue - if ($request->hasValidSignatureWhileIgnoring($ignore, ! $relative)) { + if ($request->hasValidSignatureWhileIgnoring($ignore, !$relative)) { return $next($request); } diff --git a/tests/MailTrackerTest.php b/tests/MailTrackerTest.php index 08d3503..10ffd30 100644 --- a/tests/MailTrackerTest.php +++ b/tests/MailTrackerTest.php @@ -3,7 +3,6 @@ namespace jdavidbakr\MailTracker\Tests; use Faker\Factory; -use Illuminate\Contracts\Container\Container; use Illuminate\Contracts\Debug\ExceptionHandler; use Illuminate\Database\Eloquent\Model; use Illuminate\Mail\Events\MessageSending; @@ -32,7 +31,7 @@ use jdavidbakr\MailTracker\RecordLinkClickJob; use jdavidbakr\MailTracker\RecordTrackingJob; use Mockery; -use Orchestra\Testbench\Exceptions\Handler; +use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\Attributes\Test; use Symfony\Component\Mime\Email; use Symfony\Component\Mime\Header\Headers; @@ -58,6 +57,7 @@ public function render($request, Throwable $e) { throw $e; } + public function renderForConsole($output, Throwable $e) { throw $e; @@ -486,6 +486,36 @@ public function random_string_in_link_does_not_crash() $this->get($url); } + public static function linkDataProvider() + { + return [ + 'Normal HTTP URL' => ['http://goodwebsite.com/test.html'], + 'Normal HTTPS URL' => ['https://goodwebsite.com/test.html'], + 'Normal Tel' => ['tel:+123456789'], + 'Normal Mailto' => ['mailto:test@email.com'], + ]; + } + + #[Test] + #[DataProvider('linkDataProvider')] + public function test_signed_link_redirects_to_valid_url(string $link) + { + $track = MailTracker::sentEmailModel()->newQuery()->create([ + 'hash' => Str::random(32), + 'content' => 'This is some content with a link to Good website', + ]); + + $this->disableExceptionHandling(); + + $url = URL::signedRoute('mailTracker_n', [ + 'l' => $link, + 'h' => $track->hash, + ]); + + $this->get($url) + ->assertRedirect($link); + } + #[Test] public function it_retrieves_the_message_id_from_laravel_mailer() {