Skip to content

Commit 71da975

Browse files
authored
Logout CSRF + tests (#290)
1 parent cb29fd2 commit 71da975

15 files changed

Lines changed: 107 additions & 23 deletions

config/packages/security.yaml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@ security:
2121
logout:
2222
path: app_logout
2323
target: dashboard
24+
# Without this, any page the admin visits can log them out with a plain GET
25+
enable_csrf: true
2426

2527

2628
access_control:

src/Controller/DAVController.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -187,6 +187,11 @@ public function home(): Response
187187

188188
private function initServer(string $authMethod, string $authRealm = User::DEFAULT_AUTH_REALM)
189189
{
190+
// Don't advertise the exact sabre/dav version: it appears in the `X-Sabre-Version`
191+
// header, in the `<s:sabredav-version>` element of every error body and in the HTML
192+
// browser, which only helps someone matching an installation against known advisories.
193+
\Sabre\DAV\Server::$exposeVersion = false;
194+
190195
// Get the PDO Connection of type PDO
191196
$pdo = $this->em->getConnection()->getNativeConnection();
192197

src/Entity/User.php

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,9 +20,11 @@ class User
2020

2121
/**
2222
* A username ends up in the principal URI (`principals/<username>`), so it must not carry
23-
* anything that would change that path's structure. Letters, digits and `_ . @ + ' -` are allowed:
24-
* the punctuation is what shows up in mail-derived login names. Enforced when a user is created; existing
25-
* accounts are left alone so that an odd username created before this rule stays editable.
23+
* anything that would change that path's structure. Letters, digits and `_ . @ + ' -` are
24+
* allowed: the punctuation is what shows up in mail-derived login names.
25+
*
26+
* Only checked in the `creation` validation group, so that an account whose name does not
27+
* match — an LDAP or IMAP login, say — stays editable.
2628
*/
2729
public const USERNAME_PATTERN = '/^[a-zA-Z0-9_.@+\'-]+$/';
2830

src/Plugins/DavisIMipPlugin.php

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -230,10 +230,11 @@ public function schedule(ITip\Message $itip)
230230
->replyTo(new Address($senderEmail, $mailSenderName))
231231
->subject($subject);
232232

233+
// Keep holiday auto-replies from bouncing back at invitations.
234+
$message->getHeaders()->addTextHeader('X-Auto-Response-Suppress', 'OOF, DR, RN, NRN, AutoReply');
235+
233236
if (DAV\Server::$exposeVersion) {
234-
$message->getHeaders()
235-
->addTextHeader('X-Sabre-Version: ', DAV\Version::VERSION)
236-
->addTextHeader('X-Auto-Response-Suppress', 'OOF, DR, RN, NRN, AutoReply');
237+
$message->getHeaders()->addTextHeader('X-Sabre-Version', DAV\Version::VERSION);
237238
}
238239

239240
// Now that we have everything, we can set the message body

templates/_partials/navigation.html.twig

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@
2222
👤 {{ app.user.username }}
2323
</a>
2424
<div class="dropdown-menu" aria-labelledby="navUserMenu">
25-
<a class="dropdown-item" href="{{ path('app_logout') }}">{{ "logout"|trans }}</a>
25+
<a class="dropdown-item" href="{{ path('app_logout', {_csrf_token: csrf_token('logout')}) }}">{{ "logout"|trans }}</a>
2626
</div>
2727
</li>
2828
<li class="nav-item dropdown">

templates/security/login.html.twig

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55

66
{% if app.user %}
77
<div class="mb-3">
8-
{{ "login.already"|trans({username: app.user.username}) }}, <a href="{{ path('app_logout') }}">{{ "logout"|trans }}</a>
8+
{{ "login.already"|trans({username: app.user.username}) }}, <a href="{{ path('app_logout', {_csrf_token: csrf_token('logout')}) }}">{{ "logout"|trans }}</a>
99
</div>
1010
{% else %}
1111
<div class="row justify-content-md-center">

tests/Functional/AddressBookDavTest.php

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -44,8 +44,7 @@ private function addressBookFor(string $uri): array
4444
}
4545

4646
/**
47-
* Regression test for issue #275: a display name is optional in CardDAV, but the column
48-
* was NOT NULL, so an MKCOL without {DAV:}displayname failed with a 500.
47+
* A display name is optional in CardDAV: an MKCOL without {DAV:}displayname must succeed.
4948
*/
5049
public function testAddressBookCanBeCreatedWithoutADisplayName(): void
5150
{

tests/Functional/CalendarSubscriptionTest.php

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -45,9 +45,8 @@ private function subscriptionFor(string $uri): array
4545
}
4646

4747
/**
48-
* Regression test: `calendarorder` had no default, and sabre only lists it in its INSERT
49-
* when the client sent {http://apple.com/ns/ical/}calendar-order. Subscribing without one
50-
* therefore failed with a NOT NULL violation.
48+
* sabre only lists `calendarorder` in its INSERT when the client sent
49+
* {http://apple.com/ns/ical/}calendar-order, so the column needs a default.
5150
*/
5251
public function testSubscriptionCanBeCreatedWithoutACalendarOrder(): void
5352
{

tests/Functional/Controllers/AddressBookControllerTest.php

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -206,4 +206,15 @@ public function testAddressBookWithoutADisplayNameFallsBackToItsUri(): void
206206
$this->assertResponseIsSuccessful();
207207
$this->assertAnySelectorTextContains('h5', 'nameless-book');
208208
}
209+
210+
public function testAddressBookPagesAreNotReachableAnonymously(): void
211+
{
212+
$client = static::createClient();
213+
214+
foreach (['/addressbooks/1', '/addressbooks/1/new', '/addressbooks/1/edit/1'] as $url) {
215+
$client->request('GET', $url);
216+
217+
$this->assertResponseRedirects('/login', null, $url.' must not be public');
218+
}
219+
}
209220
}

tests/Functional/Controllers/CalendarControllerTest.php

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -313,4 +313,15 @@ public function testCalendarNewIgnoresASubmittedOwner(): void
313313
$calendarRepository = static::getContainer()->get(CalendarInstanceRepository::class);
314314
$this->assertNull($calendarRepository->findOneBy(['uri' => 'hijack']));
315315
}
316+
317+
public function testCalendarPagesAreNotReachableAnonymously(): void
318+
{
319+
$client = static::createClient();
320+
321+
foreach (['/calendars/1', '/calendars/1/new', '/calendars/1/edit/1', '/calendars/1/shares/1'] as $url) {
322+
$client->request('GET', $url);
323+
324+
$this->assertResponseRedirects('/login', null, $url.' must not be public');
325+
}
326+
}
316327
}

0 commit comments

Comments
 (0)