Skip to content

Commit fc554ba

Browse files
committed
Randomize CSRF token to harden BREACH attacks
1 parent c5140c2 commit fc554ba

File tree

3 files changed

+82
-8
lines changed

3 files changed

+82
-8
lines changed

src/Symfony/Bundle/SecurityBundle/Tests/Functional/CsrfFormLoginTest.php

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,6 @@ public function testFormLoginAndLogoutWithCsrfTokens($options)
3636
$logoutLinks = $crawler->selectLink('Log out')->links();
3737
$this->assertCount(2, $logoutLinks);
3838
$this->assertStringContainsString('_csrf_token=', $logoutLinks[0]->getUri());
39-
$this->assertSame($logoutLinks[0]->getUri(), $logoutLinks[1]->getUri());
4039

4140
$client->click($logoutLinks[0]);
4241

src/Symfony/Component/Security/Csrf/CsrfTokenManager.php

Lines changed: 32 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,7 @@ public function getToken(string $tokenId)
7777
$this->storage->setToken($namespacedId, $value);
7878
}
7979

80-
return new CsrfToken($tokenId, $value);
80+
return new CsrfToken($tokenId, $this->randomize($value));
8181
}
8282

8383
/**
@@ -90,7 +90,7 @@ public function refreshToken(string $tokenId)
9090

9191
$this->storage->setToken($namespacedId, $value);
9292

93-
return new CsrfToken($tokenId, $value);
93+
return new CsrfToken($tokenId, $this->randomize($value));
9494
}
9595

9696
/**
@@ -111,11 +111,40 @@ public function isTokenValid(CsrfToken $token)
111111
return false;
112112
}
113113

114-
return hash_equals($this->storage->getToken($namespacedId), $token->getValue());
114+
return hash_equals($this->storage->getToken($namespacedId), $this->derandomize($token->getValue()));
115115
}
116116

117117
private function getNamespace(): string
118118
{
119119
return \is_callable($ns = $this->namespace) ? $ns() : $ns;
120120
}
121+
122+
private function randomize(string $value): string
123+
{
124+
$key = random_bytes(32);
125+
$value = $this->xor($value, $key);
126+
127+
return sprintf('%s.%s.%s', substr(md5($key), 0, 1 + (\ord($key[0]) % 32)), rtrim(strtr(base64_encode($key), '+/', '-_'), '='), rtrim(strtr(base64_encode($value), '+/', '-_'), '='));
128+
}
129+
130+
private function derandomize(string $value): string
131+
{
132+
$parts = explode('.', $value);
133+
if (3 !== \count($parts)) {
134+
return $value;
135+
}
136+
$key = base64_decode(strtr($parts[1], '-_', '+/'));
137+
$value = base64_decode(strtr($parts[2], '-_', '+/'));
138+
139+
return $this->xor($value, $key);
140+
}
141+
142+
private function xor(string $value, string $key): string
143+
{
144+
if (\strlen($value) > \strlen($key)) {
145+
$key = str_repeat($key, ceil(\strlen($value) / \strlen($key)));
146+
}
147+
148+
return $value ^ $key;
149+
}
121150
}

src/Symfony/Component/Security/Csrf/Tests/CsrfTokenManagerTest.php

Lines changed: 50 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ public function testGetNonExistingToken($namespace, $manager, $storage, $generat
4444

4545
$this->assertInstanceOf(CsrfToken::class, $token);
4646
$this->assertSame('token_id', $token->getId());
47-
$this->assertSame('TOKEN', $token->getValue());
47+
$this->assertNotSame('TOKEN', $token->getValue());
4848
}
4949

5050
/**
@@ -66,7 +66,34 @@ public function testUseExistingTokenIfAvailable($namespace, $manager, $storage)
6666

6767
$this->assertInstanceOf(CsrfToken::class, $token);
6868
$this->assertSame('token_id', $token->getId());
69-
$this->assertSame('TOKEN', $token->getValue());
69+
$this->assertNotSame('TOKEN', $token->getValue());
70+
}
71+
72+
/**
73+
* @dataProvider getManagerGeneratorAndStorage
74+
*/
75+
public function testRandomizeTheToken($namespace, $manager, $storage)
76+
{
77+
$storage->expects($this->any())
78+
->method('hasToken')
79+
->with($namespace.'token_id')
80+
->willReturn(true);
81+
82+
$storage->expects($this->any())
83+
->method('getToken')
84+
->with($namespace.'token_id')
85+
->willReturn('TOKEN');
86+
87+
$values = [];
88+
$lengths = [];
89+
for ($i = 0; $i < 10; ++$i) {
90+
$token = $manager->getToken('token_id');
91+
$values[] = $token->getValue();
92+
$lengths[] = \strlen($token->getValue());
93+
}
94+
95+
$this->assertCount(10, array_unique($values));
96+
$this->assertGreaterThan(2, \count(array_unique($lengths)));
7097
}
7198

7299
/**
@@ -89,13 +116,33 @@ public function testRefreshTokenAlwaysReturnsNewToken($namespace, $manager, $sto
89116

90117
$this->assertInstanceOf(CsrfToken::class, $token);
91118
$this->assertSame('token_id', $token->getId());
92-
$this->assertSame('TOKEN', $token->getValue());
119+
$this->assertNotSame('TOKEN', $token->getValue());
93120
}
94121

95122
/**
96123
* @dataProvider getManagerGeneratorAndStorage
97124
*/
98125
public function testMatchingTokenIsValid($namespace, $manager, $storage)
126+
{
127+
$storage->expects($this->exactly(2))
128+
->method('hasToken')
129+
->with($namespace.'token_id')
130+
->willReturn(true);
131+
132+
$storage->expects($this->exactly(2))
133+
->method('getToken')
134+
->with($namespace.'token_id')
135+
->willReturn('TOKEN');
136+
137+
$token = $manager->getToken('token_id');
138+
$this->assertNotSame('TOKEN', $token->getValue());
139+
$this->assertTrue($manager->isTokenValid($token));
140+
}
141+
142+
/**
143+
* @dataProvider getManagerGeneratorAndStorage
144+
*/
145+
public function testMatchingTokenIsValidWithLegacyToken($namespace, $manager, $storage)
99146
{
100147
$storage->expects($this->once())
101148
->method('hasToken')
@@ -170,7 +217,6 @@ public function testNamespaced()
170217

171218
$token = $manager->getToken('foo');
172219
$this->assertSame('foo', $token->getId());
173-
$this->assertSame('random', $token->getValue());
174220
}
175221

176222
public function getManagerGeneratorAndStorage()

0 commit comments

Comments
 (0)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy