Symfony 2.0.20 and Symfony 2.1.5 have just been released and they both contain two security fixes.

CVE-2012-6431: Routes behind a firewall are accessible even when not logged in

Affected versions

All versions from 2.0.0 to 2.0.19 are affected. Symfony 2.1.0 and later are not affected by this security issue as it was fixed in 55014a6.

Description

On the Symfony 2.0.x version, there's a security issue that allows access to routes protected by a firewall even when the user is not logged in.

Both the Routing component and the Security component uses the path returned by getPathInfo() to match a Request. The getPathInfo() returns a decoded path, but the Routing component (Symfony\Component\Routing\Matcher\UrlMatcher) decodes the path a second time; whereas the Security component, Symfony\Component\HttpFoundation\RequestMatcher, does not.

This difference causes Symfony 2.0 to be vulnerable to double encoding attacks.

Exploits

When you create a route that matches /foo and if you secure it in the security.yml configuration file, requesting /foo from a browser will redirect you to the login page if you are not already logged in. If you encode the f in the URL, /%66oo will still be secured.

But if you double-encoded the f, like in /%2566oo, then /%2566oo does not match anymore the Security rule but /%2566oo still matches the Routing route (as UrlMatcher double-decodes the path). So, when that happens, Symfony will render the /foo path even if the path is secured.

Resolution

To avoid any other regressions in the code, we have decided to re-encode the path just before matching a request to prevent the double-decoding issue.

If you are using an affected version of Symfony (any 2.0 version), you must upgrade as soon as possible by applying the following patch: 8b2c17f. If possible, we also recommend you to upgrade to Symfony 2.0.20.

Credits

I would like to thank Manuele Menozzi for reporting this security issue which came with great explanations about the issue and how to reproduce it and Tobias Schultze for providing the patch.

CVE-2012-6432: Code execution vulnerability via the "internal" routes

Affected versions

All versions of Symfony2 (2.0.X, 2.1.X, and 2.2-dev) are affected by this issue.

Your application is vulnerable if the @FrameworkBundle/Resources/config/routing/internal.xml routing file is mounted in your routing configuration and if you have not secured its routes properly (read below).

You can check if you are using the internal routes easily by running the following command:

1
2
$ php ./app/console cache:clear
$ php ./app/console router:debug _internal

If you have an error that says The route "_internal" does not exist., you are not vulnerable.

Description and Exploits

For handling ESIs (via the render tag), Symfony uses a special route named _internal, defined in @FrameworkBundle/Resources/config/routing/internal.xml.

As of Symfony 2.1, the internal routing file defines an additional route, _internal_public, to be able to manage HIncludes (also via the render tag).

As the _internal route must only be used to route URLs between your PHP application and a reverse proxy, it must be secured to avoid any access from a browser. But the _internal_public route must always be available from a browser as it should be reachable by your frontend JavaScript (of course only if you are using HIncludes in your application).

These two routes execute the same FrameworkBundle:Internal:index controller which in turn executes the controller passed as an argument in the URL. If these routes are reachable by a browser, an attacker could call them to execute protected controllers or any other service (as a controller can also be defined as a service).

Resolution

For Symfony 2.0 and 2.1, we made two changes:

  • The FrameworkBundle:Internal:index controller now only executes services for which the class name ends with Controller (it throws an exception otherwise). That limits drastically the number of classes that can be called by an attacker (if some of your controllers do not end with Controller, keep reading to learn how to fix them). You must understand that this change do not freed you from doing what is described in the next section (see 1f8c501 for the actual changes in the Symfony code).
  • The render tag now allows the usage of absolute URLs as a first argument (see the section below to learn how it helps when solving the problem in your code).

For the upcoming Symfony 2.2 version, we have taken several drastic actions to remove this problem altogether:

  • The render tag now only takes a URI as an argument and does not support controller names anymore (this means that all the fragments rendered for ESIs and HIncludes must have associated routes);
  • The @FrameworkBundle/Resources/config/routing/internal.xml and Symfony\Bundle\FrameworkBundle\Controller\InternalController files have been removed as we don't need to generate internal routes anymore;

Have a look at the changes we made in 64d43c8 for a more detailed explanation of what we changed for 2.2.

For the Symfony Standard Edition, we have tweaked the comments to warn developers about this issue.

Protecting your Code

Upgrading to the latest version of Symfony won't fix the issue. You must act according to the actions described below.

Depending on what you are using in your application, you have several options to resolve this issue :

  • Do not use the internal routes: If you are using Symfony 2.0 or Symfony 2.1 without HIncludes, and if you are not using the standalone option for the render tag, you can just remove the internal routes configuration from your routing files to protect yourself. You can check that you've done the right thing by executing the following commands (the second command must throw an error):

    1
    2
    $ php ./app/console cache:clear
    $ php ./app/console router:debug _internal
    
  • Restrict access to the internal routes: If you are using Symfony 2.0 or Symfony 2.1 without HIncludes, and if you are using the standalone option for the render tag, you must restrict access to the internal routes via your reverse proxy. Here is an example for Varnish:

    1
    2
    3
    4
    5
    6
    7
    import urlcode;
    
    vcl_recv {
        if (urlcode.decode(req.url) ~ "^/_internal") {
            error 403 "No Access";
        }
    }
    

    And another one for Nginx:

    1
    2
    3
    location ^~ /_internal {
        return 403;
    }
    
  • Updade your code: In all other cases, you must update your code by doing the following steps:

    • Step 1: Upgrade to Symfony 2.0.20 or 2.1.5 (or the most current commit if you are using Symfony 2.2-dev) depending on which version of Symfony you are using (upgrading greatly mitigates the vulnerability and should be done as soon as possible -- the remaining steps removes the vulnerability for good);

    • Step 2: Remove the internal routes configuration from your routing files;

    • Step 3: Check that the internal routes are not configured anymore by running the following commands (the second command must throw an error):

      1
      2
      $ php ./app/console cache:clear
      $ php ./app/console router:debug _internal
      
    • Step 4: Find all calls to the render tag in your templates. As an example, here is the command you can use on a Unix-like system:

      1
      $ find src/ -name "*.twig" -exec grep "{% render" {} \; -print
      
    • Step 5: Create a route for each controller used in a render tag. The public URL for the new route can be anything you like but here are some recommendations:

      • Add a special prefix/suffix to the pattern to better identify all routes that point to controllers that should only be used to generate "fragments" of content (/fragment/XXX or /XXX/YYY_fragment);
      • If the controller for the sub-request renders a content that will be included in a page that is secured, apply the same security configuration (for instance, if the main page is secured by a Symfony firewall because its path starts with /secure, use the same /secure prefix for the sub-request controller route path).
    • Step 6: Replace each render call as follows:

      Before:

      1
      2
      3
      4
      {% render "SomeBundle:Controller:action" %}
      
      {# render call with some arguments #}
      {% render "SomeBundle:Controller:action" with { 'param': 1 }, { 'standalone': true } %}
      

      After:

      1
      2
      3
      4
      5
      {# the route should have been created at step 5 #}
      {% render url("path_to_controller_router") %}
      
      {# the with argument is needed but ignored #}
      {% render url("path_to_controller_router", { 'param': 1 }) with {}, { 'standalone': true } %}
      
    • Step 7: Check that your application still works fine and deploy.

Credits

I would like to thank Victor Berchet for reporting this security issue.