My little contribution to Symfony
3Since its last update (the 2.2.0 version), Symfony has included a small piece of code of my own. To be precise, it is located in the "SensioFrameworkExtraBundle" bundle. It brings a new option to the DoctrineParamConverter object that allows to optimize the number of database requests when using the ParamConverter.
More information in the ParamConverter's doc, especially the "repository_method" option. It's mine :)
As a bonus, the pull request on GitHub.
By the way, I have a new job, now I work for Attkraktiv/Efidev, french experts in agile software development and quality, and it's awesome!
Second news, I've just migrated this website. I've left my VKS and now I have a dedicated server from kimsufi.
An alternative to CAPTCHAs with Symfony2
9Another post about security, an important topic, precisely about how to fight the form spam. I didn't want to impose CAPTCHAs on you on my blog, so I looked for an alternative method to avoid bots. I've quickly found the "honeypot button" principle via this article. Here is the idea: the bots usually submit forms without clicking button, or clicking the first button that looks like a submit button. So we display several buttons, and we verify at server-side that the right button has been used.
Symfonically speaking, we have to create a custom field type to represent the buttons. We also have to add a validation to verify that the right button has been used.
Field type creation
First of all, the buttons need to have a value, so they can be identified during the validation phase. Those values are defined in the parameters.yml
file, so they can be easily retrieved via the service container:
# app/config/parameters.yml parameters: # anti spam buttons values antispam_button: good: xuitybzerpidvfugvo bad: kuserhxvvkigyhsdsf
Then, I create my field type strictly speaking. So I create the AntispamButtonType
class. It implements ContainerAwareInterface
because we gonna need to use the service container. I define choice
as parent of my Type, because this buttons group is used for making a choice, choosing the right button to prove that you aren't a bot. Here is the class:
// src/.../BlogBundle/Form/AntispamButtonType.php class AntispamButtonType extends AbstractType implements ContainerAwareInterface { protected $container; public function __construct(ContainerInterface $container) { $this->setContainer($container); } public function setContainer(ContainerInterface $container = null) { $this->container = $container; } public function setDefaultOptions(OptionsResolverInterface $resolver) { $antispamParams = $this->container->getParameter('antispam_button'); $resolver->setDefaults(array( 'choices' => array( $antispamParams['bad'] => 'do.not.add.comment', $antispamParams['good'] => 'add.comment' ), )); } public function buildView(FormView $view, FormInterface $form, array $options) { parent::buildView($view, $form, $options); $view->vars['btn_classes'] = array('btn', 'btn btn-primary'); $view->vars['label_prefix'] = array(null, '<i class="icon-comment icon-white"></i> '); } public function getParent() { return 'choice'; } public function getName() { return 'antispambutton'; } }
So I use the service container to retrieve the button values, and I bind them to their label. I've also overridden the buildView
method to modify some rendering stuff (CSS classes, button icon).
Let's focus on rendering precisely. Symfony needs to know how this field should be displayed, so let's define a twig template:
{# src/.../BlogBundle/Resources/views/Form/fields.html.twig #} {% block antispambutton_widget %} {% for key, choice in choices %} <button name="{{ full_name }}" id="{{ id }}_{{ key }}" value="{{ choice.value }}" type="submit" class="{{ btn_classes[key] }}"> {{ label_prefix[key] is not null ? label_prefix[key]|raw : '' }}{{ choice.label|trans }} </button> {% endfor %} {% endblock %} {% block antispambutton_label %}{% endblock %}
So I create a antispambutton_widget
block ("antispambutton" must match the value returned by AntispamButtonType::getName()
) for the field rendering, and an empty antispambutton_label
block, to tell that I don't want any label. Now we need to inform the framework that there's a file to parse to render forms. That's in the config.yml
file:
# app/config/config.yml twig: form: resources: - 'MyCompanyBlogBundle:Form:fields.html.twig'
Our field type is now ready to be used! So we can instantiate it in our forms. In my case, it's CommentType
, the form to add a comment. But before we need to make this class container aware (as we did for AntispamButtonType
), since we gonna need the container to instantiate our field type (and we'll need it for the validation too, see next paragraphs). So the result is:
// src/.../BlogBundle/Form/CommentType.php class CommentType extends AbstractType implements ContainerAwareInterface { protected $container; public function __construct(ContainerInterface $container) { $this->setContainer($container); } public function setContainer(ContainerInterface $container = null) { $this->container = $container; } public function buildForm(FormBuilderInterface $builder, array $options) { $builder // other fields... ->add('button', new AntispamButtonType($this->container), array( 'property_path' => false, )) ; } }
Don't forget the property_path
option, to inform that this field isn't mapped on the Comment
object. Now the buttons are well displayed, let's talk about validation now.
Validation
The last thing to do is to verify that the used button value is the expected value. To do so, we just have to add few lines in CommentType::buildForm()
. At first, I wanted to use the CallbackValidator
class, but it's been deprecated since the 2.1 version. So we have to use the FormEvents::POST_BIND
form event (as mentioned in the deprecated comment):
// src/.../BlogBundle/Form/CommentType.php public function buildForm(FormBuilderInterface $builder, array $options) { $container = $this->container; $builder // other fields... // antispam button + validation ->add('button', new AntispamButtonType($this->container), array( 'property_path' => false, )) ->addEventListener(FormEvents::POST_BIND, function(FormEvent $event) use($container) { $form = $event->getForm(); $antispamParams = $container->getParameter('antispam_button'); if (!$form->has('button') || $form['button']->getData() !== $antispamParams['good']) { $form->addError(new FormError('spam.detected')); } }) ; }
Et voila! Now, an error is raised if the right button is not clicked. We just need to translate the spam.detected
string in the validators.XX.yml
files. So we have an ergonomic and efficient antispam system!
Edit (09/10/2012)
Further to the Jakub Lédl's pertinent comment, I've improved this code. Passing the service container this way is definitely not the right way. The field types aren't supposed to deal with the container. The thing to do is to declare the new type as a service, and to inject the parameters into it.
So let's start with the service declaration:
# src/.../BlogBundle/Resources/config/services.yml services: form.type.antispam_button: class: GregoireM\Bundle\BlogBundle\Form\AntispamButtonType arguments: - %antispam_button% tags: - { name: form.type, alias: antispambutton }
Then we have to modify the AntispamButtonType
class:
class AntispamButtonType extends AbstractType { protected $params; public function __construct(array $params) { $this->params = $params; } public function setDefaultOptions(OptionsResolverInterface $resolver) { $resolver->setDefaults(array( 'choices' => array( $this->params['bad'] => 'do.not.add.comment', $this->params['good'] => 'add.comment' ), )); } // ... }
Now we can use our field type through its alias: "antispambutton". So here is the CommentType
class, freed from its container:
// src/.../BlogBundle/Form/CommentType.php class CommentType extends AbstractType { public function buildForm(FormBuilderInterface $builder, array $options) { $builder // other fields... // antispam button + validation ->add('button', 'antispambutton', array( 'property_path' => false, )) ->addEventListener(FormEvents::POST_BIND, function(FormEvent $event) { $form = $event->getForm(); $antispamParams = $form->getConfig() ->getFormFactory() ->getType('antispambutton') ->getParams(); if (!$form->has('button') || $form['button']->getData() !== $antispamParams['good']) { $form->addError(new FormError('spam.detected')); } }) ; } // ... }
Note that we have to add the AntispamButtonType::getParams()
method, to retrieve the array parameter to use it in the validation callback. I haven't found a better way to do it...
Protect a Symfony2 project against bruteforce attacks
4To protect my brand-new blog against bruteforce attacks, I was about to create a bundle, like I did for symfony1 with the sfAntiBruteForcePlugin. Then, during my server building, I've found a tutorial that shows how to use Fail2ban to prevent SSH bruteforce attacks. For your information, Fail2ban watches log files and looks for abnormal activities. When it founds one of them, by default, it blocks the client IP during 10 minutes using iptables, and sends you an e-mail.
Custom filters can be added to Fail2ban, that's what I did to protect the login page of my blog admin zone. Here's how.
First of all, you need to know where the web server acces logs are. I use nginx, so they're here: /var/log/nginx*/*access*.log
.
Then, you need to know what's going on when a login attempt is made. I use the FOSUserBundle, so there's a POST request that is made on the /login_check
URL. It appears in the access log this way:
1.2.3.4 - - [29/Aug/2012:11:36:30 +0200] "POST /login_check HTTP/1.1" 302 ...
.
We don't need to know if the authentication has been successful or not, we consider that if the page is reached X times during a 10 minutes lapse, it's an attack. So we have all the pieces we need tout write our filter. Let's create this file: /etc/fail2ban/filter.d/nginx-login.conf
# Blocks IPs that access to authenticate using web application's login page # Scan access log for POST /login_check [Definition] failregex = <HOST> -.*POST /login_check ignoreregex =
You can see that the POST requests on URL starting with "/login_check" are watched. The last thing to do is to modify the Fail2ban configuration to use this filter. Add the following configuration at the end of /etc/fail2ban/jail.conf
:
[nginx-login] enabled = true filter = nginx-login port = http,https logpath = /var/log/nginx*/*access*.log maxretry = 5
So we tell the log files to watch, the filter to use, and the number of allowed attempts.
Don't forget to reload the service to take the new configuration into account: sudo service fail2ban force-reload
. Check the logs to be sure that it's OK: /var/log/fail2ban.log
(if your regex is invalid, it will be written here). Then try to bruteforce your site (warning, you'll be banned for 10 minutes if it works!).
Here we are, you fell better now!