security: Stored-XSS über page_url im Kontaktformular schließen (H3)

page_url aus dem anonymen Kontaktformular wird auf url:http,https
validiert; das Admin-Backend rendert nur http(s)-Links klickbar, sonst
als Text. Verhindert javascript:-URIs, die beim Admin-Klick im
Admin-Kontext liefen.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Kevin Adametz 2026-07-14 13:49:05 +00:00
parent 77f8f9cb35
commit 8f517e993d
3 changed files with 43 additions and 13 deletions

View file

@ -71,7 +71,7 @@ test('a valid contact request sends a mail to the configured recipient', functio
->assertRedirect('https://businessportal24.test/de')
->assertSessionHas('contact-status');
Mail::assertSent(ContactRequestSubmitted::class, function (ContactRequestSubmitted $mail) {
Mail::assertQueued(ContactRequestSubmitted::class, function (ContactRequestSubmitted $mail) {
return $mail->hasTo('redaktion@businessportal24.de')
&& $mail->hasReplyTo('paula@example.test')
&& $mail->senderName === 'Paula Presse';
@ -90,7 +90,7 @@ test('without a configured brand address the mail falls back to the from address
'message' => 'Ich habe eine Frage zu Ihrer Plattform und bitte um Rückruf.',
])->assertSessionHas('contact-status');
Mail::assertSent(ContactRequestSubmitted::class, fn (ContactRequestSubmitted $mail) => $mail->hasTo('fallback@pressekonto.de'));
Mail::assertQueued(ContactRequestSubmitted::class, fn (ContactRequestSubmitted $mail) => $mail->hasTo('fallback@pressekonto.de'));
});
test('a contact request from the hub domain goes to the pressekonto brand address', function () {
@ -104,7 +104,7 @@ test('a contact request from the hub domain goes to the pressekonto brand addres
'message' => 'Ich habe eine Frage zu Ihrer Plattform und bitte um Rückruf.',
])->assertSessionHas('contact-status');
Mail::assertSent(ContactRequestSubmitted::class, fn (ContactRequestSubmitted $mail) => $mail->hasTo('kontakt@pressekonto.de'));
Mail::assertQueued(ContactRequestSubmitted::class, fn (ContactRequestSubmitted $mail) => $mail->hasTo('kontakt@pressekonto.de'));
});
test('a contact request is stored in the contact queue', function () {
@ -144,12 +144,28 @@ test('a widget submission stores topic and page url', function () {
->and($message->portal)->toBe('presseecho')
->and($message->page_url)->toBe('https://presseecho.test/de/suche');
Mail::assertSent(ContactRequestSubmitted::class, function (ContactRequestSubmitted $mail) {
Mail::assertQueued(ContactRequestSubmitted::class, function (ContactRequestSubmitted $mail) {
return $mail->topicLabel === 'Fehlermeldung'
&& $mail->pageUrl === 'https://presseecho.test/de/suche';
});
});
test('a javascript page_url is rejected (stored XSS guard)', function () {
/** @var TestCase $this */
Mail::fake();
$this->post('https://presseecho.test/de/kontakt', [
'name' => 'Mallory',
'email' => 'mallory@example.test',
'message' => 'Ein ganz normal aussehender Text mit genug Zeichen.',
'topic' => 'bug_report',
'page_url' => 'javascript:alert(document.cookie)',
])->assertSessionHasErrors('page_url');
expect(ContactMessage::query()->count())->toBe(0);
Mail::assertNothingOutgoing();
});
test('an advertising enquiry from the ad slot stores the advertising topic', function () {
/** @var TestCase $this */
Mail::fake();
@ -167,7 +183,7 @@ test('an advertising enquiry from the ad slot stores the advertising topic', fun
expect($message->topic)->toBe(ContactMessageTopic::Advertising)
->and($message->portal)->toBe('presseecho');
Mail::assertSent(ContactRequestSubmitted::class, fn (ContactRequestSubmitted $mail) => $mail->topicLabel === 'Werbeanfrage');
Mail::assertQueued(ContactRequestSubmitted::class, fn (ContactRequestSubmitted $mail) => $mail->topicLabel === 'Werbeanfrage');
});
test('the spoke pages offer the advertising topic in the contact launcher', function () {
@ -190,7 +206,7 @@ test('an unknown topic is rejected', function () {
])->assertSessionHasErrors(['topic']);
expect(ContactMessage::query()->count())->toBe(0);
Mail::assertNothingSent();
Mail::assertNothingOutgoing();
});
test('honeypot submissions are silently accepted without sending mail', function () {
@ -204,7 +220,7 @@ test('honeypot submissions are silently accepted without sending mail', function
'website' => 'https://spam.example',
])->assertSessionHas('contact-status');
Mail::assertNothingSent();
Mail::assertNothingOutgoing();
expect(ContactMessage::query()->count())->toBe(0);
});
@ -218,5 +234,5 @@ test('the contact form validates its inputs', function () {
'message' => 'kurz',
])->assertSessionHasErrors(['name', 'email', 'message']);
Mail::assertNothingSent();
Mail::assertNothingOutgoing();
});