From 6860a1da74396fa68cf53fd1006a03fb41cbca15 Mon Sep 17 00:00:00 2001 From: thatside <bturchik@zaraffasoft.com> Date: Mon, 7 Aug 2017 17:52:13 +0300 Subject: [PATCH 1/4] Implemented cookie protection - some spambots do not use cookies at all --- DependencyInjection/Configuration.php | 11 +++ .../IsometriksSpamExtension.php | 17 ++++ EventListener/KernelResponseListener.php | 35 ++++++++ .../CookieValidationListener.php | 64 +++++++++++++++ .../Spam/Provider/CookieProvider.php | 36 ++++++++ .../Spam/Type/FormTypeCookieExtension.php | 82 +++++++++++++++++++ Resources/config/cookie.xml | 32 ++++++++ 7 files changed, 277 insertions(+) create mode 100644 EventListener/KernelResponseListener.php create mode 100644 Form/Extension/Spam/EventListener/CookieValidationListener.php create mode 100644 Form/Extension/Spam/Provider/CookieProvider.php create mode 100644 Form/Extension/Spam/Type/FormTypeCookieExtension.php create mode 100644 Resources/config/cookie.xml diff --git a/DependencyInjection/Configuration.php b/DependencyInjection/Configuration.php index 98b4c64..444cbc9 100644 --- a/DependencyInjection/Configuration.php +++ b/DependencyInjection/Configuration.php @@ -44,6 +44,17 @@ public function getConfigTreeBuilder() ->defaultValue('Form fields are invalid')->end() ->end() ->end() + + + ->arrayNode('cookie') + ->canBeDisabled() + ->children() + ->scalarNode('name')->defaultValue('antispam')->end() + ->booleanNode('global')->defaultFalse()->end() + ->scalarNode('message') + ->defaultValue('Something is wrong, please try again')->end() + ->end() + ->end() ->end(); return $treeBuilder; diff --git a/DependencyInjection/IsometriksSpamExtension.php b/DependencyInjection/IsometriksSpamExtension.php index b2ad811..3783608 100644 --- a/DependencyInjection/IsometriksSpamExtension.php +++ b/DependencyInjection/IsometriksSpamExtension.php @@ -25,6 +25,7 @@ public function load(array $configs, ContainerBuilder $container) $this->processTimedConfig($config['timed'], $container, $loader); $this->processHoneypotConfig($config['honeypot'], $container, $loader); + $this->processCookieConfig($config['cookie'], $container, $loader); } private function processTimedConfig(array $config, ContainerBuilder $container, XmlFileLoader $loader) @@ -61,4 +62,20 @@ private function processHoneypotConfig(array $config, ContainerBuilder $containe 'message' => $config['message'], )); } + + private function processCookieConfig(array $config, ContainerBuilder $container, XmlFileLoader $loader) + { + if (!$this->isConfigEnabled($container, $config)) { + return; + } + + $loader->load('cookie.xml'); + + $definition = $container->getDefinition('isometriks_spam.form.extension.type.cookie'); + $definition->addArgument(array( + 'name' => $config['name'], + 'global' => $config['global'], + 'message' => $config['message'], + )); + } } diff --git a/EventListener/KernelResponseListener.php b/EventListener/KernelResponseListener.php new file mode 100644 index 0000000..5976a25 --- /dev/null +++ b/EventListener/KernelResponseListener.php @@ -0,0 +1,35 @@ +<?php + + +namespace Isometriks\Bundle\SpamBundle\EventListener; + +use Isometriks\Bundle\SpamBundle\Form\Extension\Spam\Provider\CookieProvider; +use Symfony\Component\HttpFoundation\Cookie; +use Symfony\Component\HttpFoundation\Session\Session; +use Symfony\Component\HttpKernel\Event\FilterResponseEvent; + +class KernelResponseListener +{ + /** @var CookieProvider */ + private $cookieProvider; + + public function __construct($cookieProvider) + { + $this->cookieProvider = $cookieProvider; + } + + public function onKernelResponse(FilterResponseEvent $responseEvent) + { + $cookieData = $this->cookieProvider->getCookieSettings(); + if ($cookieData) { + if($cookieData['mode'] == 'add') { + $cookie = new Cookie($cookieData['name'], 1); + + $responseEvent->getResponse()->headers->setCookie($cookie); + } else if ($cookieData['mode'] == 'remove') { + $responseEvent->getResponse()->headers->removeCookie($cookieData['name']); + $this->cookieProvider->removeCookieSettings(); + } + } + } +} \ No newline at end of file diff --git a/Form/Extension/Spam/EventListener/CookieValidationListener.php b/Form/Extension/Spam/EventListener/CookieValidationListener.php new file mode 100644 index 0000000..8fc59bc --- /dev/null +++ b/Form/Extension/Spam/EventListener/CookieValidationListener.php @@ -0,0 +1,64 @@ +<?php + + +namespace Isometriks\Bundle\SpamBundle\Form\Extension\Spam\EventListener; + +use Isometriks\Bundle\SpamBundle\Form\Extension\Spam\Provider\CookieProvider; +use Symfony\Component\EventDispatcher\EventSubscriberInterface; +use Symfony\Component\Form\FormError; +use Symfony\Component\Form\FormEvent; +use Symfony\Component\Form\FormEvents; +use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\Translation\TranslatorInterface; + +class CookieValidationListener implements EventSubscriberInterface +{ + private $cookieProvider; + private $request; + private $translator; + private $translationDomain; + private $cookieName; + private $errorMessage; + + public function __construct(CookieProvider $cookieProvider, + Request $request, + TranslatorInterface $translator, + $translationDomain, + $cookieName, + $errorMessage) + { + $this->cookieProvider = $cookieProvider; + $this->request = $request; + $this->translator = $translator; + $this->translationDomain = $translationDomain; + $this->cookieName = $cookieName; + $this->errorMessage = $errorMessage; + } + + public function preSubmit(FormEvent $event) + { + $form = $event->getForm(); + + if($form->isRoot() && $form->getConfig()->getOption('compound') && !$this->request->cookies->get($this->cookieName)) { + $errorMessage = $this->errorMessage; + + if (null !== $this->translator) { + $errorMessage = $this->translator->trans($errorMessage, array(), $this->translationDomain); + } + + $form->addError(new FormError($errorMessage)); + + return; + } + + $this->cookieProvider->removeAntispamCookie($this->cookieName); + } + + + public static function getSubscribedEvents() + { + return array( + FormEvents::PRE_SUBMIT => 'preSubmit', + ); + } +} \ No newline at end of file diff --git a/Form/Extension/Spam/Provider/CookieProvider.php b/Form/Extension/Spam/Provider/CookieProvider.php new file mode 100644 index 0000000..01232cc --- /dev/null +++ b/Form/Extension/Spam/Provider/CookieProvider.php @@ -0,0 +1,36 @@ +<?php + + +namespace Isometriks\Bundle\SpamBundle\Form\Extension\Spam\Provider; + +use Symfony\Component\HttpFoundation\Session\Session; + +class CookieProvider +{ + private $session; + + public function __construct(Session $session) + { + $this->session = $session; + } + + public function setAntispamCookie($cookieName) + { + $this->session->set('antispam_cookie', array('mode' => 'add', 'name' => $cookieName)); + } + + public function removeAntispamCookie($cookieName) + { + $this->session->set('antispam_cookie', array('mode' => 'remove', 'name' => $cookieName)); + } + + public function getCookieSettings() + { + return $this->session->get('antispam_cookie'); + } + + public function removeCookieSettings() + { + $this->session->remove('antispam_cookie'); + } +} \ No newline at end of file diff --git a/Form/Extension/Spam/Type/FormTypeCookieExtension.php b/Form/Extension/Spam/Type/FormTypeCookieExtension.php new file mode 100644 index 0000000..b2f1f40 --- /dev/null +++ b/Form/Extension/Spam/Type/FormTypeCookieExtension.php @@ -0,0 +1,82 @@ +<?php + + +namespace Isometriks\Bundle\SpamBundle\Form\Extension\Spam\Type; + +use Isometriks\Bundle\SpamBundle\Form\Extension\Spam\EventListener\CookieValidationListener; +use Isometriks\Bundle\SpamBundle\Form\Extension\Spam\Provider\CookieProvider; +use Symfony\Component\Form\AbstractTypeExtension; +use Symfony\Component\Form\FormBuilderInterface; +use Symfony\Component\Form\FormInterface; +use Symfony\Component\Form\FormView; +use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\HttpFoundation\Session\Session; +use Symfony\Component\OptionsResolver\OptionsResolver; +use Symfony\Component\OptionsResolver\OptionsResolverInterface; +use Symfony\Component\Translation\TranslatorInterface; + +class FormTypeCookieExtension extends AbstractTypeExtension +{ + private $cookieProvider; + private $request; + private $session; + private $translator; + private $translationDomain; + private $defaults; + + public function __construct(CookieProvider $cookieProvider, + Request $request, + Session $session, + TranslatorInterface $translator, + $translationDomain, + array $defaults) + { + $this->cookieProvider = $cookieProvider; + $this->request = $request; + $this->session = $session; + $this->translator = $translator; + $this->translationDomain = $translationDomain; + $this->defaults = $defaults; + } + + public function buildForm(FormBuilderInterface $builder, array $options) + { + if (!$options['cookie']) { + return; + } + + $builder + ->addEventSubscriber(new CookieValidationListener( + $this->cookieProvider, + $this->request, + $this->translator, + $this->translationDomain, + $options['cookie_name'], + $options['cookie_message'] + )); + } + + public function finishView(FormView $view, FormInterface $form, array $options) + { + $this->cookieProvider->setAntispamCookie($options['cookie_name']); + } + + public function setDefaultOptions(OptionsResolverInterface $resolver) + { + $this->configureOptions($resolver); + } + + public function configureOptions(OptionsResolver $resolver) + { + $resolver->setDefaults(array( + 'cookie' => $this->defaults['global'], + 'cookie_name' => $this->defaults['name'], + 'cookie_message' => $this->defaults['message'], + )); + } + + public function getExtendedType() + { + return 'form'; + } +} \ No newline at end of file diff --git a/Resources/config/cookie.xml b/Resources/config/cookie.xml new file mode 100644 index 0000000..40125cc --- /dev/null +++ b/Resources/config/cookie.xml @@ -0,0 +1,32 @@ +<?xml version="1.0" ?> + +<container xmlns="http://symfony.com/schema/dic/services" + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:schemaLocation="http://symfony.com/schema/dic/services http://symfony.com/schema/dic/services/services-1.0.xsd"> + + <parameters> + <parameter key="isometriks_spam.form.extension.type.cookie.class">Isometriks\Bundle\SpamBundle\Form\Extension\Spam\Type\FormTypeCookieExtension</parameter> + <parameter key="isometriks_spam.form.extension.provider.cookie.class">Isometriks\Bundle\SpamBundle\Form\Extension\Spam\Provider\CookieProvider</parameter> + <parameter key="isometriks_spam.event_listener.kernel_response.class">Isometriks\Bundle\SpamBundle\EventListener\KernelResponseListener</parameter> + </parameters> + + <services> + <service id="isometriks_spam.form.extension.provider.cookie" class="%isometriks_spam.form.extension.provider.cookie.class%"> + <argument type="service" id="session" /> + </service> + + <service id="isometriks_spam.form.extension.type.cookie" class="%isometriks_spam.form.extension.type.cookie.class%" scope="request"> + <tag name="form.type_extension" alias="form" /> + <argument type="service" id="isometriks_spam.form.extension.provider.cookie" /> + <argument type="service" id="request" /> + <argument type="service" id="session" /> + <argument type="service" id="translator.default" /> + <argument>%validator.translation_domain%</argument> + </service> + + <service id="isometriks_spam.exception_listener" class="%isometriks_spam.event_listener.kernel_response.class%"> + <tag name="kernel.event_listener" event="kernel.response" method="onKernelResponse" /> + <argument type="service" id="isometriks_spam.form.extension.provider.cookie" /> + </service> + </services> +</container> From 8e0311eb1fbde3d5fb4c59706e315d698a84bd28 Mon Sep 17 00:00:00 2001 From: thatside <bturchik@zaraffasoft.com> Date: Tue, 8 Aug 2017 11:31:25 +0300 Subject: [PATCH 2/4] Clearing cookie after protection --- EventListener/KernelResponseListener.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/EventListener/KernelResponseListener.php b/EventListener/KernelResponseListener.php index 5976a25..df2a531 100644 --- a/EventListener/KernelResponseListener.php +++ b/EventListener/KernelResponseListener.php @@ -5,7 +5,6 @@ use Isometriks\Bundle\SpamBundle\Form\Extension\Spam\Provider\CookieProvider; use Symfony\Component\HttpFoundation\Cookie; -use Symfony\Component\HttpFoundation\Session\Session; use Symfony\Component\HttpKernel\Event\FilterResponseEvent; class KernelResponseListener @@ -28,6 +27,7 @@ public function onKernelResponse(FilterResponseEvent $responseEvent) $responseEvent->getResponse()->headers->setCookie($cookie); } else if ($cookieData['mode'] == 'remove') { $responseEvent->getResponse()->headers->removeCookie($cookieData['name']); + $responseEvent->getResponse()->headers->clearCookie($cookieData['name']); $this->cookieProvider->removeCookieSettings(); } } From d7328eff89643d0b83269801c39295ca50142651 Mon Sep 17 00:00:00 2001 From: thatside <bturchik@zaraffasoft.com> Date: Wed, 11 Oct 2017 16:29:04 +0300 Subject: [PATCH 3/4] Updated cookie protection to work with Symfony 3 --- .../Spam/Type/FormTypeCookieExtension.php | 27 ++++++++++--------- .../Spam/Type/FormTypeHoneypotExtension.php | 12 ++++----- .../Spam/Type/FormTypeTimedSpamExtension.php | 13 ++++----- Resources/config/cookie.xml | 12 ++++----- Resources/config/honeypot.xml | 2 +- Resources/config/timed.xml | 4 +-- 6 files changed, 36 insertions(+), 34 deletions(-) diff --git a/Form/Extension/Spam/Type/FormTypeCookieExtension.php b/Form/Extension/Spam/Type/FormTypeCookieExtension.php index b2f1f40..12ea1cf 100644 --- a/Form/Extension/Spam/Type/FormTypeCookieExtension.php +++ b/Form/Extension/Spam/Type/FormTypeCookieExtension.php @@ -6,13 +6,13 @@ use Isometriks\Bundle\SpamBundle\Form\Extension\Spam\EventListener\CookieValidationListener; use Isometriks\Bundle\SpamBundle\Form\Extension\Spam\Provider\CookieProvider; use Symfony\Component\Form\AbstractTypeExtension; +use Symfony\Component\Form\Extension\Core\Type\FormType; use Symfony\Component\Form\FormBuilderInterface; use Symfony\Component\Form\FormInterface; use Symfony\Component\Form\FormView; -use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\HttpFoundation\RequestStack; use Symfony\Component\HttpFoundation\Session\Session; use Symfony\Component\OptionsResolver\OptionsResolver; -use Symfony\Component\OptionsResolver\OptionsResolverInterface; use Symfony\Component\Translation\TranslatorInterface; class FormTypeCookieExtension extends AbstractTypeExtension @@ -24,21 +24,22 @@ class FormTypeCookieExtension extends AbstractTypeExtension private $translationDomain; private $defaults; - public function __construct(CookieProvider $cookieProvider, - Request $request, - Session $session, - TranslatorInterface $translator, - $translationDomain, - array $defaults) - { + public function __construct( + CookieProvider $cookieProvider, + RequestStack $requestStack, + Session $session, + TranslatorInterface $translator, + $translationDomain, + array $defaults + ) { $this->cookieProvider = $cookieProvider; - $this->request = $request; + $this->request = $requestStack->getCurrentRequest(); $this->session = $session; $this->translator = $translator; $this->translationDomain = $translationDomain; $this->defaults = $defaults; } - + public function buildForm(FormBuilderInterface $builder, array $options) { if (!$options['cookie']) { @@ -61,7 +62,7 @@ public function finishView(FormView $view, FormInterface $form, array $options) $this->cookieProvider->setAntispamCookie($options['cookie_name']); } - public function setDefaultOptions(OptionsResolverInterface $resolver) + public function setDefaultOptions(OptionsResolver $resolver) { $this->configureOptions($resolver); } @@ -77,6 +78,6 @@ public function configureOptions(OptionsResolver $resolver) public function getExtendedType() { - return 'form'; + return FormType::class; } } \ No newline at end of file diff --git a/Form/Extension/Spam/Type/FormTypeHoneypotExtension.php b/Form/Extension/Spam/Type/FormTypeHoneypotExtension.php index 378eab1..1318171 100644 --- a/Form/Extension/Spam/Type/FormTypeHoneypotExtension.php +++ b/Form/Extension/Spam/Type/FormTypeHoneypotExtension.php @@ -10,7 +10,6 @@ use Symfony\Component\Form\FormInterface; use Symfony\Component\Form\FormView; use Symfony\Component\OptionsResolver\OptionsResolver; -use Symfony\Component\OptionsResolver\OptionsResolverInterface; use Symfony\Component\Translation\TranslatorInterface; class FormTypeHoneypotExtension extends AbstractTypeExtension @@ -19,10 +18,11 @@ class FormTypeHoneypotExtension extends AbstractTypeExtension private $translationDomain; private $defaults; - public function __construct(TranslatorInterface $translator, - $translationDomain, - array $defaults) - { + public function __construct( + TranslatorInterface $translator, + $translationDomain, + array $defaults + ) { $this->translator = $translator; $this->translationDomain = $translationDomain; $this->defaults = $defaults; @@ -74,7 +74,7 @@ public function finishView(FormView $view, FormInterface $form, array $options) } } - public function setDefaultOptions(OptionsResolverInterface $resolver) + public function setDefaultOptions(OptionsResolver $resolver) { $this->configureOptions($resolver); } diff --git a/Form/Extension/Spam/Type/FormTypeTimedSpamExtension.php b/Form/Extension/Spam/Type/FormTypeTimedSpamExtension.php index 05eeb11..92b73bd 100644 --- a/Form/Extension/Spam/Type/FormTypeTimedSpamExtension.php +++ b/Form/Extension/Spam/Type/FormTypeTimedSpamExtension.php @@ -20,11 +20,12 @@ class FormTypeTimedSpamExtension extends AbstractTypeExtension private $translationDomain; private $defaults; - public function __construct(TimedSpamProviderInterface $timeProvider, - TranslatorInterface $translator, - $translationDomain, - array $defaults) - { + public function __construct( + TimedSpamProviderInterface $timeProvider, + TranslatorInterface $translator, + $translationDomain, + array $defaults + ) { $this->timeProvider = $timeProvider; $this->translator = $translator; $this->translationDomain = $translationDomain; @@ -59,7 +60,7 @@ public function finishView(FormView $view, FormInterface $form, array $options) } } - public function setDefaultOptions(OptionsResolverInterface $resolver) + public function setDefaultOptions(OptionsResolver $resolver) { $this->configureOptions($resolver); } diff --git a/Resources/config/cookie.xml b/Resources/config/cookie.xml index 40125cc..d8b444f 100644 --- a/Resources/config/cookie.xml +++ b/Resources/config/cookie.xml @@ -14,16 +14,16 @@ <service id="isometriks_spam.form.extension.provider.cookie" class="%isometriks_spam.form.extension.provider.cookie.class%"> <argument type="service" id="session" /> </service> - - <service id="isometriks_spam.form.extension.type.cookie" class="%isometriks_spam.form.extension.type.cookie.class%" scope="request"> - <tag name="form.type_extension" alias="form" /> + + <service id="isometriks_spam.form.extension.type.cookie" class="%isometriks_spam.form.extension.type.cookie.class%" public="true"> + <tag name="form.type_extension" extended-type="Symfony\Component\Form\Extension\Core\Type\FormType" alias="form" /> <argument type="service" id="isometriks_spam.form.extension.provider.cookie" /> - <argument type="service" id="request" /> + <argument type="service" id="request_stack" /> <argument type="service" id="session" /> - <argument type="service" id="translator.default" /> + <argument type="service" id="translator" /> <argument>%validator.translation_domain%</argument> </service> - + <service id="isometriks_spam.exception_listener" class="%isometriks_spam.event_listener.kernel_response.class%"> <tag name="kernel.event_listener" event="kernel.response" method="onKernelResponse" /> <argument type="service" id="isometriks_spam.form.extension.provider.cookie" /> diff --git a/Resources/config/honeypot.xml b/Resources/config/honeypot.xml index aa51b12..801881b 100644 --- a/Resources/config/honeypot.xml +++ b/Resources/config/honeypot.xml @@ -9,7 +9,7 @@ </parameters> <services> - <service id="isometriks_spam.form.extension.type.honeypot" class="%isometriks_spam.form.extension.type.honeypot.class%"> + <service id="isometriks_spam.form.extension.type.honeypot" class="%isometriks_spam.form.extension.type.honeypot.class%" public="true"> <tag name="form.type_extension" extended-type="Symfony\Component\Form\Extension\Core\Type\FormType" /> <argument type="service" id="translator" /> <argument>%validator.translation_domain%</argument> diff --git a/Resources/config/timed.xml b/Resources/config/timed.xml index 1fb6518..d95c0c3 100644 --- a/Resources/config/timed.xml +++ b/Resources/config/timed.xml @@ -14,8 +14,8 @@ <argument type="service" id="session" /> </service> - <service id="isometriks_spam.form.extension.type.timed_spam" class="%isometriks_spam.form.extension.type.timed_spam.class%"> - <tag name="form.type_extension" extended-type="Symfony\Component\Form\Extension\Core\Type\FormType" /> + <service id="isometriks_spam.form.extension.type.timed_spam" class="%isometriks_spam.form.extension.type.timed_spam.class%" public="true"> + <tag name="form.type_extension" extended-type="Symfony\Component\Form\Extension\Core\Type\FormType" alias="form" /> <argument type="service" id="isometriks_spam.form.extension.provider.timed_spam" /> <argument type="service" id="translator" /> <argument>%validator.translation_domain%</argument> From 39771181b757ca60d41306d8334cb838ef7805b9 Mon Sep 17 00:00:00 2001 From: thatside <bturchik@zaraffasoft.com> Date: Wed, 11 Oct 2017 16:39:24 +0300 Subject: [PATCH 4/4] Updated README --- README.md | 33 +++++++++++++++++++++++++++++++-- 1 file changed, 31 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index e318022..7828896 100644 --- a/README.md +++ b/README.md @@ -89,7 +89,7 @@ If the field is filled out, then the form is invalid. You can optionally choose to use a class name to hide the form element as well in case the bot tries to check the style attribute. -```yml +```YAML isometriks_spam: honeypot: field: email_address @@ -123,9 +123,38 @@ public function configureOptions(OptionsResolver $resolver) } ``` -### Twig Form Error message rendering +### Cookie Spam Prevention + +Most of spam bots can't use cookies so we could check cookie value on each request. + +If there is no cookie set - then the request is invalid and set by bots. +These cookies are not used for any tracking and are deleted after form is submitted and handled. + +Configuration: +```YAML +isometriks_spam: + cookie: + name: antispam_cookie + global: false + message: Something is wrong, please try again ``` + +Usage: + +```php +public function configureOptions(OptionsResolver $resolver) +{ + $resolver->setDefaults(array( + 'cookie' => true, + // ... + )); +} +``` + +### Twig Form Error message rendering + +```twig {% if form.vars.errors is not empty %} <div class="alert alert-danger has-error">{{ form_errors(form) }}</div> {% endif %}