Skip to content

Commit 492336f

Browse files
committed
CsrfCounterMeasure: Only validate transmitted tokens
1 parent 6dd2276 commit 492336f

File tree

2 files changed

+102
-25
lines changed

2 files changed

+102
-25
lines changed

src/Common/CsrfCounterMeasure.php

Lines changed: 33 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,14 @@
22

33
namespace ipl\Web\Common;
44

5+
use Error;
56
use ipl\Html\Contract\FormElement;
6-
use ipl\Html\Form;
7+
use ipl\Html\FormElement\HiddenElement;
78

89
trait CsrfCounterMeasure
910
{
1011
/**
11-
* Create a form element to counter measure CSRF attacks
12+
* Create a form element to countermeasure CSRF attacks
1213
*
1314
* @param string $uniqueId A unique ID that persists through different requests
1415
*
@@ -21,28 +22,35 @@ protected function createCsrfCounterMeasure($uniqueId)
2122
$seed = random_bytes(16);
2223
$token = base64_encode($seed) . '|' . hash($hashAlgo, $uniqueId . $seed);
2324

24-
/** @var Form $this */
25-
return $this->createElement(
26-
'hidden',
27-
'CSRFToken',
28-
[
29-
'ignore' => true,
30-
'required' => true,
31-
'value' => $token,
32-
'validators' => ['Callback' => function ($token) use ($uniqueId, $hashAlgo) {
33-
if (strpos($token, '|') === false) {
34-
die('Invalid CSRF token provided');
35-
}
36-
37-
list($seed, $hash) = explode('|', $token);
38-
39-
if ($hash !== hash($hashAlgo, $uniqueId . base64_decode($seed))) {
40-
die('Invalid CSRF token provided');
41-
}
42-
43-
return true;
44-
}]
45-
]
46-
);
25+
$options = [
26+
'ignore' => true,
27+
'required' => true,
28+
'validators' => ['Callback' => function ($token) use ($uniqueId, $hashAlgo) {
29+
if (empty($token) || strpos($token, '|') === false) {
30+
throw new Error('Invalid CSRF token provided');
31+
}
32+
33+
list($seed, $hash) = explode('|', $token);
34+
35+
if ($hash !== hash($hashAlgo, $uniqueId . base64_decode($seed))) {
36+
throw new Error('Invalid CSRF token provided');
37+
}
38+
39+
return true;
40+
}]
41+
];
42+
43+
$element = new class ('CSRFToken', $options) extends HiddenElement {
44+
public function hasValue(): bool
45+
{
46+
return true; // The validator must run even if the value is empty
47+
}
48+
};
49+
50+
$element->getAttributes()->registerAttributeCallback('value', function () use ($token) {
51+
return $token;
52+
});
53+
54+
return $element;
4755
}
4856
}
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
<?php
2+
3+
namespace ipl\Tests\Web\Common;
4+
5+
use ipl\Html\Contract\FormElement;
6+
use ipl\Html\Form;
7+
use ipl\Html\FormElement\HiddenElement;
8+
use ipl\Tests\Web\TestCase;
9+
use ipl\Web\Common\CsrfCounterMeasure;
10+
11+
class CsrfCounterMeasureTest extends TestCase
12+
{
13+
public function testTokenCreation()
14+
{
15+
$token = $this->createElement();
16+
17+
$this->assertInstanceOf(HiddenElement::class, $token);
18+
$this->assertMatchesRegularExpression(
19+
'/ value="[^"]+\|[^"]+"/',
20+
(string) $token,
21+
'The value is not rendered or does not contain a seed and a hash'
22+
);
23+
}
24+
25+
public function testMissingToken()
26+
{
27+
$token = $this->createElement();
28+
29+
$this->assertNull($token->getValue(), 'The default value must only be set after the form is rendered');
30+
31+
$this->expectError();
32+
$this->expectErrorMessage('Invalid CSRF token provided');
33+
34+
$token->isValid();
35+
}
36+
37+
public function testValidToken()
38+
{
39+
$token = $this->createElement();
40+
41+
$this->assertSame(1, preg_match('/ value="([^"]+)"/', (string) $token, $matches));
42+
43+
$token->setValue($matches[1]);
44+
$this->assertTrue($token->isValid(), 'Token should be valid with the default value');
45+
}
46+
47+
public function testInvalidToken()
48+
{
49+
$token = $this->createElement();
50+
51+
$token->setValue('invalid');
52+
53+
$this->expectError();
54+
$this->expectErrorMessage('Invalid CSRF token provided');
55+
56+
$token->isValid();
57+
}
58+
59+
private function createElement(): FormElement
60+
{
61+
$form = new class extends Form {
62+
use CsrfCounterMeasure {
63+
createCsrfCounterMeasure as public;
64+
}
65+
};
66+
67+
return $form->createCsrfCounterMeasure('uniqueId');
68+
}
69+
}

0 commit comments

Comments
 (0)