8000 Randomize CSRF token to harden BREACH attacks · symfony/symfony@fc554ba · GitHub
[go: up one dir, main page]

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)
0