Skip to content

Commit 74c7012

Browse files
committed
add match captcha ttl, translate validation messages
1 parent 3ae7b86 commit 74c7012

File tree

8 files changed

+86
-23
lines changed

8 files changed

+86
-23
lines changed

config/install/translations/admin.csv

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,7 @@
6363
"form_builder_type.birthday_type","Birthday Type","Geburtstag Element"
6464
"form_builder_type.recaptcha_v3","reCAPTCHA v3 Field","reCAPTCHA v3 Feld"
6565
"form_builder_type.math_captcha","Math Captcha Field","Mathematisches Captcha Feld"
66+
"form_builder_type_field.math_captcha.validation_message_trans_note","Validation messages will be translated via pimcore translation manager: 'The given answer is not correct', 'Captcha has expired due to inactivity. Please refresh the page and try again.'","Validierungsmeldungen werden mit dem Pimcore Translation-Manager übersetzt: 'The given answer is not correct', 'Captcha has expired due to inactivity. Please refresh the page and try again.'"
6667
"form_builder_type_field.math_captcha.difficulty","Difficulty","Schwierigkeit"
6768
"form_builder_type.friendly_captcha","Friendly Captcha Field","Friendly Captcha Feld"
6869
"form_builder_type.cloudflare_turnstile","Cloudflare Turnstile Field","Cloudflare Turnstile Feld"

config/types/type/math_captcha.yaml

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,11 +13,15 @@ form_builder:
1313
options.data: ~
1414
options.value: ~
1515
options.difficulty:
16-
display_group_id: attributes
16+
display_group_id: base
1717
type: select
1818
label: 'form_builder_type_field.math_captcha.difficulty'
1919
config:
2020
options:
2121
- ['easy','easy']
2222
- ['normal','normal']
23-
- ['hard','hard']
23+
- ['hard','hard']
24+
options.validation_message_trans_note:
25+
display_group_id: base
26+
type: label
27+
label: 'form_builder_type_field.math_captcha.validation_message_trans_note'

docs/03_SpamProtection.md

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -96,8 +96,10 @@ Since no session storage is required, it can be easily integrated with minimal o
9696
> However, it will give you time, to set up stronger spam protection like
9797
> recaptcha, turnstile or friendly captcha (which are all supported by this bundle) to get rid of smart bots!
9898
99+
### Encryption Secret
100+
99101
> [!IMPORTANT]
100-
> Match Captcha requires a valid encryption key!
102+
> Math Captcha requires a valid encryption key!
101103
> It uses the `%pimcore.encryption.secret%` as default, but you're able to set a dedicated one:
102104
103105
```yaml
@@ -107,6 +109,18 @@ form_builder:
107109
encryption_secret: 'my-very-long-encryption-secret'
108110
```
109111
112+
### Math Captcha TTL | Expiration
113+
To prevent replay attacks in a long-term view, a math captcha gets invalided after 30 minutes.
114+
115+
If you want to change the value, you need to update the configuration
116+
117+
```yaml
118+
form_builder:
119+
spam_protection:
120+
math_captcha:
121+
hash_ttl: 30 # 30 minutes (default value)
122+
```
123+
110124
***
111125
112126
## Email Checker

src/DependencyInjection/Configuration.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -666,6 +666,7 @@ private function buildSpamProductionNode(): NodeDefinition
666666
->addDefaultsIfNotSet()
667667
->children()
668668
->scalarNode('encryption_secret')->defaultNull()->end()
669+
->integerNode('hash_ttl')->defaultValue(30)->end()
669670
->end()
670671
->end()
671672

src/Form/Type/MathCaptchaType.php

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -31,22 +31,24 @@ public function __construct(protected MathCaptchaProcessor $mathCaptchaProcessor
3131

3232
public function buildForm(FormBuilderInterface $builder, array $options): void
3333
{
34-
$challenge = $this->mathCaptchaProcessor->generateChallenge($options['difficulty']);
34+
$stamp = $this->mathCaptchaProcessor->generateStamp();
35+
$challenge = $this->mathCaptchaProcessor->generateChallenge($options['difficulty'], $stamp);
3536

3637
$challengeFieldOptions = [
37-
'label' => $challenge['user_challenge'],
38+
'label' => $challenge['user_challenge'],
3839
'label_attr' => [
3940
'class' => 'math-captcha-challenge-label'
4041
],
4142
];
4243

4344
if ($challenge['hash'] === null) {
4445
$challengeFieldOptions['attr']['disabled'] = true;
45-
$challengeFieldOptions['label'] = 'No encryption secret found. cannot create challenge';
46+
$challengeFieldOptions['label'] = 'No encryption secret found. cannot create challenge.';
4647
}
4748

4849
$builder->add('challenge', TextType::class, $challengeFieldOptions);
4950
$builder->add('hash', HiddenType::class, ['data' => $challenge['hash']]);
51+
$builder->add('stamp', HiddenType::class, ['data' => $stamp]);
5052
}
5153

5254
public function buildView(FormView $view, FormInterface $form, array $options): void

src/Tool/MathCaptchaProcessor.php

Lines changed: 33 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -13,17 +13,22 @@
1313

1414
namespace FormBuilderBundle\Tool;
1515

16+
use Carbon\Carbon;
1617
use FormBuilderBundle\Configuration\Configuration;
1718

1819
class MathCaptchaProcessor implements MathCaptchaProcessorInterface
1920
{
21+
public const VALIDATION_STATE_VALID = 'valid';
22+
public const VALIDATION_STATE_INVALID_VALUE = 'invalid_value';
23+
public const VALIDATION_STATE_EXPIRED = 'expired';
24+
2025
public function __construct(
2126
protected ?string $pimcoreEncryptionSecret,
2227
protected Configuration $configuration
2328
) {
2429
}
2530

26-
public function generateChallenge(string $difficulty): array
31+
public function generateChallenge(string $difficulty, string $stamp): array
2732
{
2833
$numbers = match ($difficulty) {
2934
default => [
@@ -45,16 +50,30 @@ public function generateChallenge(string $difficulty): array
4550

4651
return [
4752
'user_challenge' => $challenge,
48-
'hash' => $this->encryptChallenge(array_sum($numbers))
53+
'hash' => $this->encryptChallenge(array_sum($numbers), $stamp)
4954
];
5055
}
5156

52-
public function verify(int $challenge, string $hash): bool
57+
public function generateStamp(): string
58+
{
59+
return Carbon::now()->toIso8601String();
60+
}
61+
62+
public function verify(int $challenge, string $hash, string $stamp): string
5363
{
54-
return $hash === $this->encryptChallenge($challenge);
64+
$now = Carbon::now();
65+
$date = Carbon::parse($stamp)->addMinutes($this->getHashTtl());
66+
67+
if ($now->isAfter($date)) {
68+
return self::VALIDATION_STATE_EXPIRED;
69+
}
70+
71+
return $hash === $this->encryptChallenge($challenge, $stamp)
72+
? self::VALIDATION_STATE_VALID
73+
: self::VALIDATION_STATE_INVALID_VALUE;
5574
}
5675

57-
public function encryptChallenge(int $challenge): ?string
76+
public function encryptChallenge(int $challenge, string $stamp): ?string
5877
{
5978
$encryptionSecret = $this->getEncryptionSecret();
6079

@@ -65,7 +84,7 @@ public function encryptChallenge(int $challenge): ?string
6584
return hash_hmac(
6685
'sha256',
6786
(string) $challenge,
68-
$encryptionSecret
87+
sprintf('%s%s', $encryptionSecret, $stamp)
6988
);
7089
}
7190

@@ -81,4 +100,12 @@ private function getEncryptionSecret(): ?string
81100

82101
return $encryptionSecret;
83102
}
103+
104+
private function getHashTtl(): int
105+
{
106+
$config = $this->configuration->getConfig('spam_protection');
107+
$mathCaptchaConfig = $config['math_captcha'];
108+
109+
return $mathCaptchaConfig['hash_ttl'];
110+
}
84111
}

src/Validator/Constraints/MathCaptcha.php

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,5 +17,6 @@
1717

1818
final class MathCaptcha extends Constraint
1919
{
20-
public string $message = 'The given answer is not correct.';
20+
public string $invalidValueMessage = 'The given answer is not correct.';
21+
public string $expiredMessage = 'Captcha has expired due to inactivity. Please refresh the page and try again.';
2122
}

src/Validator/Constraints/MathCaptchaValidator.php

Lines changed: 23 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -17,11 +17,14 @@
1717
use Symfony\Component\Validator\Constraint;
1818
use Symfony\Component\Validator\ConstraintValidator;
1919
use Symfony\Component\Validator\Exception\UnexpectedTypeException;
20+
use Symfony\Contracts\Translation\TranslatorInterface;
2021

2122
final class MathCaptchaValidator extends ConstraintValidator
2223
{
23-
public function __construct(protected MathCaptchaProcessor $mathCaptchaProcessor)
24-
{
24+
public function __construct(
25+
protected TranslatorInterface $translator,
26+
protected MathCaptchaProcessor $mathCaptchaProcessor
27+
) {
2528
}
2629

2730
public function validate(mixed $value, Constraint $constraint): void
@@ -36,20 +39,30 @@ public function validate(mixed $value, Constraint $constraint): void
3639

3740
$challenge = $value['challenge'] ?? null;
3841
$hash = $value['hash'] ?? null;
42+
$stamp = $value['stamp'] ?? null;
43+
44+
$validationState = $this->validateCaptcha($challenge, $hash, $stamp);
3945

40-
if (!$this->validateCaptcha($challenge, $hash)) {
41-
$this->context->buildViolation($constraint->message)
42-
->setParameter('{{ value }}', $this->formatValue($value))
43-
->addViolation();
46+
if ($validationState === MathCaptchaProcessor::VALIDATION_STATE_VALID) {
47+
return;
4448
}
49+
50+
$validationMessage = $validationState === MathCaptchaProcessor::VALIDATION_STATE_EXPIRED
51+
? $constraint->expiredMessage
52+
: $constraint->invalidValueMessage;
53+
54+
$this->context
55+
->buildViolation($this->translator->trans($validationMessage))
56+
->setParameter('{{ value }}', $this->formatValue($value))
57+
->addViolation();
4558
}
4659

47-
private function validateCaptcha(?string $value, $hash): bool
60+
private function validateCaptcha(?string $value, ?string $hash, ?string $stamp): string
4861
{
49-
if ($value === '' || $hash === null) {
50-
return false;
62+
if ($value === '' || $value === null || $hash === null || $stamp === null) {
63+
return MathCaptchaProcessor::VALIDATION_STATE_INVALID_VALUE;
5164
}
5265

53-
return $this->mathCaptchaProcessor->verify((int) $value, $hash);
66+
return $this->mathCaptchaProcessor->verify((int) $value, $hash, $stamp);
5467
}
5568
}

0 commit comments

Comments
 (0)