From 15f4b2c0d8337b41d807545d26f498da1d6f7223 Mon Sep 17 00:00:00 2001 From: boban Date: Sat, 16 May 2026 01:16:47 +0200 Subject: [PATCH] fix(qr): wifi/vcard injection protection, remove dead code Co-Authored-By: Claude Sonnet 4.6 --- app/Domains/QrCode/Actions/GenerateQrCode.php | 2 -- app/Domains/QrCode/Models/QrCode.php | 1 - app/Domains/QrCode/Services/QrRenderer.php | 17 ++++++++++-- tests/Unit/QrCode/QrRendererTest.php | 26 +++++++++++++++++++ 4 files changed, 41 insertions(+), 5 deletions(-) diff --git a/app/Domains/QrCode/Actions/GenerateQrCode.php b/app/Domains/QrCode/Actions/GenerateQrCode.php index 81476ce..29d2bdf 100644 --- a/app/Domains/QrCode/Actions/GenerateQrCode.php +++ b/app/Domains/QrCode/Actions/GenerateQrCode.php @@ -18,8 +18,6 @@ class GenerateQrCode User $creator, ?int $linkId = null, ): QrCode { - $data = $this->renderer->buildPayload($type, $payload); - return QrCode::create([ 'workspace_id' => $workspaceId, 'link_id' => $linkId, diff --git a/app/Domains/QrCode/Models/QrCode.php b/app/Domains/QrCode/Models/QrCode.php index 5a58578..8053bf7 100644 --- a/app/Domains/QrCode/Models/QrCode.php +++ b/app/Domains/QrCode/Models/QrCode.php @@ -16,7 +16,6 @@ class QrCode extends Model protected $casts = [ 'payload' => 'array', 'style' => 'array', - 'ulid' => 'string', ]; protected static function booted(): void diff --git a/app/Domains/QrCode/Services/QrRenderer.php b/app/Domains/QrCode/Services/QrRenderer.php index e50f79f..b8b4274 100644 --- a/app/Domains/QrCode/Services/QrRenderer.php +++ b/app/Domains/QrCode/Services/QrRenderer.php @@ -35,14 +35,27 @@ class QrRenderer return match ($type) { 'url' => $data['url'], 'text' => $data['text'], - 'wifi' => "WIFI:T:{$data['encryption']};S:{$data['ssid']};P:{$data['password']};;", + 'wifi' => sprintf( + 'WIFI:T:%s;S:%s;P:%s;;', + $this->escapeWifiField($data['encryption'] ?? ''), + $this->escapeWifiField($data['ssid'] ?? ''), + $this->escapeWifiField($data['password'] ?? ''), + ), 'vcard' => $this->buildVcard($data), default => $data['url'] ?? '', }; } + private function escapeWifiField(string $value): string + { + return str_replace(['\\', ';', ',', '"'], ['\\\\', '\\;', '\\,', '\\"'], $value); + } + private function buildVcard(array $d): string { - return "BEGIN:VCARD\nVERSION:3.0\nFN:{$d['name']}\nEMAIL:{$d['email']}\nTEL:{$d['phone']}\nEND:VCARD"; + $name = str_replace(["\r", "\n"], '', $d['name'] ?? ''); + $email = str_replace(["\r", "\n"], '', $d['email'] ?? ''); + $phone = str_replace(["\r", "\n"], '', $d['phone'] ?? ''); + return "BEGIN:VCARD\nVERSION:3.0\nFN:{$name}\nEMAIL:{$email}\nTEL:{$phone}\nEND:VCARD"; } } diff --git a/tests/Unit/QrCode/QrRendererTest.php b/tests/Unit/QrCode/QrRendererTest.php index b2fa3ee..2341538 100644 --- a/tests/Unit/QrCode/QrRendererTest.php +++ b/tests/Unit/QrCode/QrRendererTest.php @@ -16,3 +16,29 @@ it('generates png as base64', function () { expect($png)->toStartWith('data:image/png;base64,'); }); + +it('builds url payload', function () { + $renderer = new QrRenderer; + expect($renderer->buildPayload('url', ['url' => 'https://example.com'])) + ->toBe('https://example.com'); +}); + +it('builds wifi payload', function () { + $renderer = new QrRenderer; + expect($renderer->buildPayload('wifi', [ + 'encryption' => 'WPA', + 'ssid' => 'My Network', + 'password' => 'pass;word', + ]))->toBe('WIFI:T:WPA;S:My Network;P:pass\\;word;;'); +}); + +it('sanitizes vcard injection', function () { + $renderer = new QrRenderer; + $payload = $renderer->buildPayload('vcard', [ + 'name' => "Evil\nINJECTED:property", + 'email' => 'x@example.com', + 'phone' => '123', + ]); + // Newline stripped so "INJECTED:property" cannot appear as a separate vCard property line + expect($payload)->not->toContain("\nINJECTED"); +});