Un des sites sur lesquels j'ai un formulaire de contact Symfony commence à recevoir pas mal de messages de Spam.

Pour éviter cela je peux mettre en place un reCaptcha mais je préfère essayer de régler les problèmes de mon côté avant d'importer encore plus d'éléments.

Mon formulaire de départ était plus tôt simple :

public function buildForm(FormBuilderInterface $builder, array $options)
    {
        $builder
            ->add('name', TextType::class,
                [
                    'required' => false,
                    'label' => 'form.contact.name',
                    'translation_domain' => 'Default'
                ]
            )
            ->add('email', EmailType::class,
                [
                    'required' => false,
                    'label' => 'form.contact.email',
                    'translation_domain' => 'Default',
                ]
            )
            ->add('url', UrlType::class,
                [
                    'required' => false,
                    'label' => 'form.contact.url',
                    'translation_domain' => 'Default'
                ]
            )
            ->add('message', TextType::class,
                [
                'required' => false,
                 'label' => 'form.contact.message',
                  'translation_domain' => 'Default'
                  ]
            );
    }

Basé sur mon entité Message :

/**
 * @ORM\Entity(repositoryClass="App\Repository\MessageRepository")
 */
class Message
{
    use TimestampableEntity;

    private $id;
    private $name;

    /**
     * @ORM\Column(type="string", length=255)
     * @Assert\NotBlank
     * @Assert\Email(
     *     message = "The email '{{ value }}' is not a valid email.",
     *       mode="strict"
     * )
     */
    private $email;

    private $url;

    private $message;
 // ...
}

Le formulaire est envoyé par du AJAX et traité dans un Controlleur :

/**
     * @Route("message", name="send_message", options={"expose"=true}, methods={"POST"})
     * @param Request $request
     * @param TranslatorInterface $translator
     * @return JsonResponse
     */
    public function sendMessage(Request $request, TranslatorInterface $translator): JsonResponse
    {
        $messageForm = $this->createForm(MessageType::class, null);
        $messageForm->handleRequest($request);

        if ($messageForm->isSubmitted() && $messageForm->isValid()) {
            /** @var Message $message */
            $message = $messageForm->getData();

            $this->manager->persist($message);

            $this->manager->flush();

            return new JsonResponse($translator->trans('form.contact.sent', [], 'Default'));
        } else {
            $errors = $this->getErrorsFromForm($messageForm);
            return new JsonResponse($errors, Response::HTTP_BAD_REQUEST);
        }
    }

Maintenant rajoutons des choses pour perturber les robots !

J'imagine que les robots reconnaissent les inputs qui ont email en nom et adorent les remplir. Je vais donc garder mon champ email et rajouter un champ qui à un nom aléatoire qui détiendra le vrai email de l'envoyeur s’il a bien rempli le formulaire.

Le nouveau champ de mon entité Message a les assertions d'email, et le champ email ne les a plus :

class Message
{

    public const EMAIL_HIDDEN_INPUT = 'fhjgiz46';

    //...

    /**
     * @ORM\Column(type="string", length=255)
     */
    private $email;

    /**
     * @Assert\NotBlank
     * @Assert\Email(
     *     message = "The email '{{ value }}' is not a valid email.",
     *       mode="strict"
     * )
     */
    private $fhjgiz46;

    //...
}

Ce nouveau champ n'est pas mappé et ne sera donc pas présent en base de données. J'ai rajouté ce champ dans mon FormType :

    public function buildForm(FormBuilderInterface $builder, array $options)
    {
        $builder
//...
            ->add('email', EmailType::class, ['required' => false, 'attr' => ['class' => 'email']])
            ->add(Message::EMAIL_HIDDEN_INPUT, EmailType::class,
                [
                    'required' => false,
                    'label' => 'form.contact.email',
                    'translation_domain' => 'Default',
                ]
            )
//...
    }

Ce champ aura une classe css .email sur lequel je rajoute quelques règles afin de le rendre invisible pour l'homme :

input.email {
    height: 0;
    margin: 0;
    text-decoration: none;
    border: none;
}

Dans le controlleur je veux vérifier si l'input email est rempli. Si c'est le cas ça veux dire que c'est un robot qui essaie de remplir le formulaire et nous ne voulons pas envoyer d'email.

S’il n'est pas rempli mais le nouveau champ aléatoire l'est nous allons passer la valeur de ce champ au paramètre email de l'entité :

public function sendMessage(Request $request, TranslatorInterface $translator): JsonResponse
    {
        $messageForm = $this->createForm(MessageType::class, null);
        $messageForm->handleRequest($request);
        if ($messageForm->isSubmitted() && $messageForm->isValid()) {

            /** @var Message $message */
            $message = $messageForm->getData();

            //Check honeypot
            if ($message->getEmail()) {
                $this->logger->alert(sprintf('Spam tried with %s from %s', $message->getEmail(), $request->getClientIp()));
                return new JsonResponse($translator->trans('form.contact.error', [], 'Default'), Response::HTTP_NOT_ACCEPTABLE);
            }

            //If honeypot not set swap the hidden email input
            if ($message->getFhjgiz46()) {
                $message->setEmail($message->getFhjgiz46());
            }

            $this->manager->persist($message);

            $this->manager->flush();

            return new JsonResponse($translator->trans('form.contact.sent', [], 'Default'));
        } else {
            $errors = $this->getErrorsFromForm($messageForm);
            return new JsonResponse($errors, Response::HTTP_BAD_REQUEST);
        }
    }

Pour garder un œil sur les robots j'ai rajouté un appel au Logger qui va me rajouter une ligne à chaque fois qu'un robot tombe dans le piège. Et en surveillant les logs on s'aperçoit très vite des tentatives :

cat var/log/prod-2021-03-19.log  | grep ALERT
[2021-03-19T13:17:31.314259+01:00] app.ALERT: Spam tried with ashleegravell@aol.com from 212.116.86.206 [] []
[2021-03-19T13:25:27.326076+01:00] app.ALERT: Spam tried with noahreyna2018@icloud.com from 213.230.70.146 [] []
[2021-03-19T13:49:27.272809+01:00] app.ALERT: Spam tried with webcowboy@verizon.net from 103.27.207.36 [] []
[2021-03-19T14:21:14.832344+01:00] app.ALERT: Spam tried with speedee44@aol.com from 63.237.228.123 [] []
[2021-03-19T14:43:25.047792+01:00] app.ALERT: Spam tried with jane_eisen2007@yahoo.com from 196.46.199.5 [] []

S’il y a vraiment beaucoup de Spam qui arrivent depuis une adresse IP je rajouterai un peu de Fail2Ban.