fix(qr): wifi/vcard injection protection, remove dead code

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
main
boban 2026-05-16 01:16:47 +02:00
parent d3b777d8e8
commit 15f4b2c0d8
4 changed files with 41 additions and 5 deletions

View File

@ -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,

View File

@ -16,7 +16,6 @@ class QrCode extends Model
protected $casts = [
'payload' => 'array',
'style' => 'array',
'ulid' => 'string',
];
protected static function booted(): void

View File

@ -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";
}
}

View File

@ -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");
});