Symfony2: Security enhancements part I

Posted on by Matthias Noback

There's a much more detailed chapter about this subject in my book A Year With Symfony.

When working with Symfony2, you already have many of the finest tools for securing your web application. There are cases however that require you to add that extra bit. In this post I will point you to the right extension points within a Symfony2 project (or any other project which uses the Security Component for that matter).

Install NelmioSecurityBundle

See the README of the NelmioSecurityBundle. It contains many add-ons for your project, to sign/encrypt cookies, force SSL, prevent clickjacking and prevent untrusted redirects.

Log Security Information

When anything noteworthy related to security happens inside your application, log it to the "security" channel of the application logger. This way you can easily filter your logs in case of a security emergency. When injecting the logger into a service, do it like this:

class SecurityRelatedClass
{
    private $logger;

    public function __construct(LoggerInterface $logger = null)
    {
        // there may be no logger defined, depending on your site's configuration

        $this->logger = $logger;
    }

    public function doSomething()
    {
        if ($this->logger instanceof LoggerInterface) {
            // don't make it an error, since there is no failure of the system here

            $this->logger->info('Interesting security-related information');
        }

        // ...
    }
}

In the service definition add a tag and mention the channel:

<service id="matthias_security.security_related"
         class="Matthias\ApplicationBundle\SecurityRelatedClass">
    <argument type="service" id="logger" on-invalid="null" />
    <tag name="monolog.logger" channel="security" />
</service>

Enhance CSRF Protection

Each form by default has a "_token" field containing a token that is unique per user since it is based on the session ID. Usually it is the same for all forms of your application. But it is possible to make it different for each form. Use the "intention" option for form types:

use Symfony\Component\Form\AbstractType;
use Symfony\Component\OptionsResolver\OptionsResolverInterface;

class RegisterType extends AbstractType
{
    public function setDefaultOptions(OptionsResolverInterface $resolver)
    {
        $resolver->setDefaults(array(
            'intention' => 'register'
        ));
    }
}

Use a Real Form Type for the Login Form

I don't know why the Symfony documentation tells you to circumvent the entire Form Component and just write your login form in plain and simple HTML. I think it is much better to create your own form type, register it as a service and then render your form in a Twig template, using {{ form_widget(form) }}, just like any other form. This will (almost) automatically give you the benefit of a standard CSRF token. To make it work, you will also have to adapt your login form type's options and the configuration for each "form_login" section in security.yml:

security:
    firewalls:
        my_firewall:
            pattern:    ^/
            form_login:
                username_parameter: "login[username]"
                password_parameter: "login[password]"
                csrf_parameter: "login[_token]"
                csrf_provider: form.csrf_provider
                intention: authentication

As you can see, we need to provide the name of the form fields containing the username, password and CSRF token. Make sure the intention in the form type matches the one in the security configuration:

use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolverInterface;

class LoginType extends AbstractType
{
    public function buildForm(FormBuilderInterface $builder, array $options)
    {
        $builder
            ->add('username', 'text')
            ->add('password', 'password');
    }

    public function setDefaultOptions(OptionsResolverInterface $resolver)
    {
        $resolver->setDefaults(array(
            'intention' => 'authentication'
        ));
    }

    public function getName()
    {
        return 'login';
    }
}

In part II: session settings, session invalidation and keeping track of failed login attempts.

PHP Security Symfony2 authentication CSRF logging
Comments
This website uses MailComments: you can send your comments to this post by email. Read more about MailComments, including suggestions for writing your comments (in HTML or Markdown).
Israel Carberry

Add this to your LoginType class:

private $targetUrl;
public function __construct($targetUrl = '')
{
$this->targetUrl = $targetUrl;
}

...and a setAction to the form builder:

public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->setAction($this->targetUrl)
->add('username', 'text')
...

Inject the login_check route into the service:
(in YAML):
arguments: [ @=service('router').generate('login_check') ]
(in XML):
<argument type="expression">service('router').generate('login_check')</argument>

As defined in your routing:
login_check:
path: /login_check

Mohammad Shokri Khanghah

Greate article
U saved my day
Thank u

Mark Fox

I'm a little confused, doesn't Symfony generate a unique CSRF token by default per form? I could be mistaken but after taking a look at my own project is seems like when you don't specify an intention each form receives a unique CSRF token. The intention option is interesting, are there are areas/uses other than form_login where you can use intention in a similar way?

Matthias Noback

This has likely changed in the meantime! If you see different CSRF tokens for different forms, than everything is okay now :)

HardCoreMore

Thank you man. Finally. I spent hours figuring out why login did not work. That part of using just simple twig html for log in form is stupid. And they never said in the documentation how to configure security.yml file properly like you did:

form_login:
username_parameter: "login[username]"
password_parameter: "login[password]"
csrf_parameter: "login[_token]"
csrf_provider: form.csrf_provider
intention: authentication

Thanks alot for that. The docs about log in should be definitevely updated by Symfony guys. And I mean it. In current condition is unusable for newcomers.

Thanks again.

Henrik Bjornskov

Actually i wrote a component enhancement for this about a year ago, which also will set the error on the form type when displaying. https://github.com/Vandpibe...

Matthias Noback

Excellent ideas! Thanks for sharing.

Hugo Hamon

Your blog is a gold mine! Thanks for sharing all these valuable information ;)

Matthias Noback

Thank you too!

Pedro Junior

Very nice tips!

I've tried a lot to render login form with a Form Type, now i know how to do!

Thanks

Matthias Noback

Thanks!

greg

The documentation do not use the form component to not intoduce a hard dependency between the form and the security components.

More over the the csrf is not needed on a login form by definition. Of course you can use it to prevent stupid robot to brut force your authentication system. But it is not mandatory.

Great blog post anyway

Matthias Noback

Thanks Greg. It's a good idea to write separately about the components, but I always thought that "The Book" didn't make this distinction (while "The Components" does). I think it would be good to encourage people to use the Form component whenever they create a form in an application. Any generally implemented form behavior will then also be inherited by the login form.
Also, adding a CSRF-token to the login form will indeed only prevent "stupid" attacks. Once an attacker has done a single attack by hand (i.e. in a browser), he can forge subsequent request with the token received from the first attack. He can than easily automate the following attacks. Symfony does not allow for CSRF-tokens which can only be used once. This would help a lot in slowing down the attacker. See also https://github.com/symfony/... I plan to write about this later.

Pádraic Brady

Greg, why would you not have a CSRF token on a login form? Would you like the attacker to use a network of users to mount a brute force or timing attack which would circumvent any max attempts or time delay defenses?

Yes, use a CSRF - on ALL forms.