8000 [Validator] Convert encoding to UTF-8 when needed in NotPwnedValidator · symfony/symfony@f0c51f8 · GitHub
[go: up one dir, main page]

Skip to content

Commit f0c51f8

Browse files
committed
[Validator] Convert encoding to UTF-8 when needed in NotPwnedValidator
1 parent 6456b96 commit f0c51f8

File tree

2 files changed

+62
-25
lines changed

2 files changed

+62
-25
lines changed

src/Symfony/Component/Validator/Constraints/NotCompromisedPasswordValidator.php

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,14 +31,16 @@ class NotCompromisedPasswordValidator extends ConstraintValidator
3131
private const RANGE_API = 'https://api.pwnedpasswords.com/range/%s';
3232

3333
private $httpClient;
34+
private $charset;
3435

35-
public function __construct(HttpClientInterface $httpClient = null)
36+
public function __construct(HttpClientInterface $httpClient = null, string $charset = 'UTF-8')
3637
{
3738
if (null === $httpClient && !class_exists(HttpClient::class)) {
3839
throw new \LogicException(sprintf('The "%s" class requires the "HttpClient" component. Try running "composer require symfony/http-client".', self::class));
3940
}
4041

4142
$this->httpClient = $httpClient ?? HttpClient::create();
43+
$this->charset = $charset;
4244
}
4345

4446
/**
@@ -61,6 +63,10 @@ public function validate($value, Constraint $constraint)
6163
return;
6264
}
6365

66+
if ('UTF-8' !== $this->charset) {
67+
$value = mb_convert_encoding($value, 'UTF-8', $this->charset);
68+
}
69+
6470
$hash = strtoupper(sha1($value));
6571
$hashPrefix = substr($hash, 0, 5);
6672
$url = sprintf(self::RANGE_API, $hashPrefix);

src/Symfony/Component/Validator/Tests/Constraints/NotCompromisedPasswordValidatorTest.php

Lines changed: 55 additions & 24 deletions
Original file line number< 10000 /th>Diff line numberDiff line change
@@ -28,40 +28,22 @@ class NotCompromisedPasswordValidatorTest extends ConstraintValidatorTestCase
2828
private const PASSWORD_TRIGGERING_AN_ERROR_RANGE_URL = 'https://api.pwnedpasswords.com/range/3EF27'; // https://api.pwnedpasswords.com/range/3EF27 is the range for the value "apiError"
2929
private const PASSWORD_LEAKED = 'maman';
3030
private const PASSWORD_NOT_LEAKED = ']<0585"%sb^5aa$w6!b38",,72?dp3r4\45b28Hy';
31+
private const PASSWORD_NON_UTF8_LEAKED = 'мама';
32+
private const PASSWORD_NON_UTF8_NOT_LEAKED = 'м<в0dp3r4\45b28Hy';
3133

3234
private const RETURN = [
3335
'35E033023A46402F94CFB4F654C5BFE44A1:1',
3436
'35F079CECCC31812288257CD770AA7968D7:53',
35-
'36039744C253F9B2A4E90CBEDB02EBFB82D:5', // this is the matching line, password: maman
37+
'36039744C253F9B2A4E90CBEDB02EBFB82D:5', // UTF-8 leaked password: maman
38+
'273CA8A2A78C9B2D724144F4FAF4D221C86:6', // ISO-8859-5 leaked password: мама
3639
'3686792BBC66A72D40D928ED15621124CFE:7',
3740
'36EEC709091B810AA240179A44317ED415C:2',
3841
];
3942

4043
protected function createValidator()
4144
{
42-
$httpClientStub = $this->createMock(HttpClientInterface::class);
43-
$httpClientStub->method('request')->will(
44-
$this->returnCallback(function (string $method, string $url): ResponseInterface {
45-
if (self::PASSWORD_TRIGGERING_AN_ERROR_RANGE_URL === $url) {
46-
throw new class('Problem contacting the Have I been Pwned API.') extends \Exception implements ServerExceptionInterface {
47-
public function getResponse(): ResponseInterface
48-
{
49-
throw new \RuntimeException('Not implemented');
50-
}
51-
};
52-
}
53-
54-
$responseStub = $this->createMock(ResponseInterface::class);
55-
$responseStub
56-
->method('getContent')
57-
->willReturn(implode("\r\n", self::RETURN));
58-
59-
return $responseStub;
60-
})
61-
);
62-
63-
// Pass HttpClient::create() instead of this mock to run the tests against the real API
64-
return new NotCompromisedPasswordValidator($httpClientStub);
45+
// Pass HttpClient::create() instead of the mock to run the tests against the real API
46+
return new NotCompromisedPasswordValidator($this->createHttpClientStub());
6547
}
6648

6749
public function testNullIsValid()
@@ -112,6 +94,29 @@ public function testValidPassword()
11294
$this->assertNoViolation();
11395
}
11496

97+
public function testNonUtf8CharsetValid()
98+
{
99+
$validator = new NotCompromisedPasswordValidator($this->createHttpClientStub(), 'ISO-8859-5');
100+
$validator->validate(mb_convert_encoding(self::PASSWORD_NON_UTF8_NOT_LEAKED, 'ISO-8859-5', 'UTF-8'), new NotCompromisedPassword());
101+
102+
$this->assertNoViolation();
103+
}
104+
105+
public function testNonUtf8CharsetInvalid()
106+
{
107+
$constraint = new NotCompromisedPassword();
108+
109+
$this->context = $this->createContext();
110+
111+
$validator = new NotCompromisedPasswordValidator($this->createHttpClientStub(), 'ISO-8859-5');
112+
$validator->initialize($this->context);
113+
$validator->validate(mb_convert_encoding(self::PASSWORD_NON_UTF8_LEAKED, 'ISO-8859-5', 'UTF-8'), $constraint);
114+
115+
$this->buildViolation($constraint->message)
116+
->setCode(NotCompromisedPassword::COMPROMISED_PASSWORD_ERROR)
117+
->assertRaised();
118+
}
119+
115120
/**
116121
* @expectedException \Symfony\Component\Validator\Exception\UnexpectedTypeException
117122
*/
@@ -142,4 +147,30 @@ public function testApiErrorSkipped()
142147
$this->validator->validate(self::PASSWORD_TRIGGERING_AN_ERROR, new NotCompromisedPassword(['skipOnError' => true]));
143148
$this->assertTrue(true); // No exception have been thrown
144149
}
150+
151+
private function createHttpClientStub(): HttpClientInterface
152+
{
153+
$httpClientStub = $this->createMock(HttpClientInterface::class);
154+
$httpClientStub->method('request')->will(
155+
$this->returnCallback(function (string $method, string $url): ResponseInterface {
156+
if (self::PASSWORD_TRIGGERING_AN_ERROR_RANGE_URL === $url) {
157+
throw new class('Problem contacting the Have I been Pwned API.') extends \Exception implements ServerExceptionInterface {
158+
public function getResponse(): ResponseInterface
159+
{
160+
throw new \RuntimeException('Not implemented');
161+
}
162+
};
163+
}
164+
165+
$responseStub = $this->createMock(ResponseInterface::class);
166+
$responseStub
167+
->method('getContent')
168+
->willReturn(implode("\r\n", self::RETURN));
169+
170+
return $responseStub;
171+
})
172+
);
173+
174+
return $httpClientStub;
175+
}
145176
}

0 commit comments

Comments
 (0)
0