-
-
Notifications
You must be signed in to change notification settings - Fork 9.4k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
[Validator] Add a
NotSuspicious
constraint to validate a string is …
…not a spoof attempt
- Loading branch information
Showing
4 changed files
with
247 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
82 changes: 82 additions & 0 deletions
82
src/Symfony/Component/Validator/Constraints/NotSuspicious.php
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,82 @@ | ||
<?php | ||
|
||
/* | ||
* This file is part of the Symfony package. | ||
* | ||
* (c) Fabien Potencier <fabien@symfony.com> | ||
* | ||
* For the full copyright and license information, please view the LICENSE | ||
* file that was distributed with this source code. | ||
*/ | ||
|
||
namespace Symfony\Component\Validator\Constraints; | ||
|
||
use Symfony\Component\Validator\Constraint; | ||
use Symfony\Component\Validator\Exception\LogicException; | ||
|
||
/** | ||
* @Annotation | ||
* | ||
* @Target({"PROPERTY", "METHOD", "ANNOTATION"}) | ||
* | ||
* @author Mathieu Lechat <mathieu.lechat@les-tilleuls.coop> | ||
*/ | ||
#[\Attribute(\Attribute::TARGET_PROPERTY | \Attribute::TARGET_METHOD | \Attribute::IS_REPEATABLE)] | ||
class NotSuspicious extends Constraint | ||
{ | ||
/** | ||
* Check that a string satisfies the requirements for the specified restriction level | ||
* (by default {@see \Spoofchecker::HIGHLY_RESTRICTIVE}). | ||
*/ | ||
public const CHECK_RESTRICTION_LEVEL = 16; | ||
|
||
/** | ||
* Check a string for the presence of invisible characters such as zero-width spaces, | ||
* or character sequences that are likely not to display such as multiple occurrences of the same non-spacing mark. | ||
*/ | ||
public const CHECK_INVISIBLE = \Spoofchecker::INVISIBLE; | ||
|
||
/** Check a string contains only characters allowed by the configured profile. */ | ||
public const CHECK_CHAR_LIMIT = \Spoofchecker::CHAR_LIMIT; | ||
|
||
/** | ||
* Check that a string does not mix numbers from different numbering systems; | ||
* for example “8” (Digit Eight) and “৪” (Bengali Digit Four). | ||
*/ | ||
public const CHECK_MIXED_NUMBERS = 128; | ||
|
||
/** | ||
* Check that a string does not have a combining character following a character in which it would be hidden; | ||
* for example “i” (Latin Small Letter I) followed by a U+0307 (Combining Dot Above). | ||
*/ | ||
public const CHECK_HIDDEN_OVERLAY = 256; | ||
|
||
public $message = 'This value is suspicious.'; | ||
public int $checks = self::CHECK_RESTRICTION_LEVEL | self::CHECK_INVISIBLE | self::CHECK_CHAR_LIMIT | self::CHECK_MIXED_NUMBERS | self::CHECK_HIDDEN_OVERLAY; | ||
public int $restrictionLevel = \Spoofchecker::HIGHLY_RESTRICTIVE; | ||
public array $profileLocales = []; | ||
public bool $addCurrentLocaleToProfile = true; | ||
|
||
public function __construct( | ||
array $options = null, | ||
string $message = null, | ||
int $checks = null, | ||
int $restrictionLevel = null, | ||
array $profileLocales = null, | ||
bool $addCurrentLocaleToProfile = null, | ||
array $groups = null, | ||
mixed $payload = null | ||
) { | ||
if (!class_exists(\Spoofchecker::class)) { | ||
throw new LogicException('The intl extension is required to use the NotSuspicious constraint.'); | ||
} | ||
|
||
parent::__construct($options, $groups, $payload); | ||
|
||
$this->message ??= $message; | ||
$this->checks ??= $checks; | ||
$this->restrictionLevel ??= $restrictionLevel; | ||
$this->profileLocales ??= $profileLocales; | ||
$this->addCurrentLocaleToProfile ??= $addCurrentLocaleToProfile; | ||
} | ||
} |
77 changes: 77 additions & 0 deletions
77
src/Symfony/Component/Validator/Constraints/NotSuspiciousValidator.php
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,77 @@ | ||
<?php | ||
|
||
/* | ||
* This file is part of the Symfony package. | ||
* | ||
* (c) Fabien Potencier <fabien@symfony.com> | ||
* | ||
* For the full copyright and license information, please view the LICENSE | ||
* file that was distributed with this source code. | ||
*/ | ||
|
||
namespace Symfony\Component\Validator\Constraints; | ||
|
||
use Symfony\Component\Validator\Constraint; | ||
use Symfony\Component\Validator\ConstraintValidator; | ||
use Symfony\Component\Validator\Exception\UnexpectedTypeException; | ||
use Symfony\Component\Validator\Exception\UnexpectedValueException; | ||
use Symfony\Contracts\Translation\LocaleAwareInterface; | ||
|
||
/** | ||
* @author Mathieu Lechat <mathieu.lechat@les-tilleuls.coop> | ||
*/ | ||
class NotSuspiciousValidator extends ConstraintValidator implements LocaleAwareInterface | ||
{ | ||
private string $locale; | ||
|
||
public function validate(mixed $value, Constraint $constraint) | ||
{ | ||
if (!$constraint instanceof NotSuspicious) { | ||
throw new UnexpectedTypeException($constraint, NotSuspicious::class); | ||
} | ||
|
||
if (null === $value || '' === $value) { | ||
return; | ||
} | ||
|
||
if (!\is_scalar($value) && !$value instanceof \Stringable) { | ||
throw new UnexpectedValueException($value, 'string'); | ||
} | ||
|
||
$value = (string) $value; | ||
if ('' === $value) { | ||
return; | ||
} | ||
|
||
$checker = new \Spoofchecker(); | ||
|
||
$checker->setRestrictionLevel($constraint->restrictionLevel); | ||
|
||
$allowedLocales = $constraint->profileLocales; | ||
if ($constraint->addCurrentLocaleToProfile) { | ||
$allowedLocales[] = $this->locale; | ||
} | ||
$checker->setAllowedLocales(implode(',', $allowedLocales)); | ||
|
||
$checker->setChecks($constraint->checks); | ||
|
||
if (!$checker->isSuspicious($value)) { | ||
return; | ||
} | ||
|
||
$this->context->buildViolation($constraint->message) | ||
->setParameter('{{ value }}', $this->formatValue($value)) | ||
->addViolation() | ||
; | ||
} | ||
|
||
public function setLocale(string $locale) | ||
{ | ||
$this->locale = $locale; | ||
} | ||
|
||
public function getLocale(): string | ||
{ | ||
return $this->locale; | ||
} | ||
} |
87 changes: 87 additions & 0 deletions
87
src/Symfony/Component/Validator/Tests/Constraints/NotSuspiciousValidatorTest.php
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,87 @@ | ||
<?php | ||
|
||
/* | ||
* This file is part of the Symfony package. | ||
* | ||
* (c) Fabien Potencier <fabien@symfony.com> | ||
* | ||
* For the full copyright and license information, please view the LICENSE | ||
* file that was distributed with this source code. | ||
*/ | ||
|
||
namespace Symfony\Component\Validator\Tests\Constraints; | ||
|
||
use Symfony\Component\Validator\Constraints\NotSuspicious; | ||
use Symfony\Component\Validator\Constraints\NotSuspiciousValidator; | ||
use Symfony\Component\Validator\Test\ConstraintValidatorTestCase; | ||
|
||
/** | ||
* @requires extension intl | ||
* | ||
* @extends ConstraintValidatorTestCase<NotSuspiciousValidator> | ||
*/ | ||
class NotSuspiciousValidatorTest extends ConstraintValidatorTestCase | ||
{ | ||
protected function createValidator(): NotSuspiciousValidator | ||
{ | ||
$validator = new NotSuspiciousValidator(); | ||
$validator->setLocale(\Locale::getDefault()); | ||
|
||
return $validator; | ||
} | ||
|
||
public function testNonSuspiciousStrings() | ||
{ | ||
$this->validator->validate('à', new NotSuspicious()); | ||
|
||
$this->assertNoViolation(); | ||
} | ||
|
||
/** | ||
* @dataProvider provideSuspiciousStrings | ||
*/ | ||
public function testSuspiciousStrings(string $string, array $options) | ||
{ | ||
$this->validator->validate($string, new NotSuspicious(['message' => 'myMessage'] + $options)); | ||
|
||
$this->buildViolation('myMessage') | ||
->setParameter('{{ value }}', '"'.$string.'"') | ||
->assertRaised(); | ||
} | ||
|
||
public static function provideSuspiciousStrings(): iterable | ||
{ | ||
yield 'Fails restriction level check because of character outside ASCII range' => ['à', | ||
['restrictionLevel' => \Spoofchecker::ASCII], | ||
]; | ||
yield 'Fails restriction level check because of mixed-script string' => ['àㄚ', [ | ||
'restrictionLevel' => \Spoofchecker::SINGLE_SCRIPT_RESTRICTIVE, | ||
'profileLocales' => ['zh_Hant_TW'], | ||
]]; | ||
yield 'Fails restriction level check because of disallowed Armenian script' => ['àԱ', [ | ||
'restrictionLevel' => \Spoofchecker::HIGHLY_RESTRICTIVE, | ||
'profileLocales' => ['hy_AM'], | ||
]]; | ||
yield 'Fails restriction level check because of disallowed Greek script' => ['àπ', [ | ||
'restrictionLevel' => \Spoofchecker::MODERATELY_RESTRICTIVE, | ||
'profileLocales' => ['el_GR'], | ||
]]; | ||
yield 'Fails restriction level check because of Greek script absent from profile' => ['àπ', [ | ||
'checks' => NotSuspicious::CHECK_RESTRICTION_LEVEL, | ||
'restrictionLevel' => \Spoofchecker::MINIMALLY_RESTRICTIVE, | ||
]]; | ||
|
||
yield 'Fails INVISIBLE check because of duplicated non-spacing mark' => ['à̀', [ | ||
'checks' => NotSuspicious::CHECK_INVISIBLE, | ||
]]; | ||
yield 'Fails CHAR_LIMIT check because of Greek script absent from profile' => ['àπ', [ | ||
'checks' => NotSuspicious::CHECK_CHAR_LIMIT, | ||
]]; | ||
yield 'Fails MIXED_NUMBERS check because of different numbering systems' => ['8৪', [ | ||
'checks' => NotSuspicious::CHECK_MIXED_NUMBERS, | ||
]]; | ||
yield 'Fails HIDDEN_OVERLAY check because of hidden combining character' => ['i̇', [ | ||
'checks' => NotSuspicious::CHECK_HIDDEN_OVERLAY, | ||
]]; | ||
} | ||
} |