Forcing SSL and HTTPS with Redirects on Symfony 1.2

Programming, symfony 1 September 2009 | Comments

Forcing SSL on certain modules and actions used to be pretty simple with Symfony’s sfSslRequirementPlugin, however since Symfony 1.2 came out – it isn’t necessarily compatible. I took a look around the internet for options, however I was rather unsatisfied with the options. Because I need to be able to secure modules and actions, I took it upon myself to create a better way to secure modules.

The code I wrote It is based off of Say No To Flash’s filter that they wrote, however I was generally displeased with their method. It loops over the list twice, and is generally a garble of logic that is a little bit hard to understand, and could probably be done better.

I was planning on making this system more complicated by specifying if SSL was enabled or disabled by default, however that became too distorted in the code to make good, logical sense that was easy to read and maintain.

Edit your filters.yml File

Open up your apps/app_name/config/filters.yml file, and add in at the bottom:

sslFilter:
  class: sslFilter

This code tells Symfony to load and execute the filter you’ll be creating.

Edit your app.yml File

Open the apps/app_name/config/app.yml file, and add the following at the end:

all:
  ssl:
    strict: true
    modules:
      - { module: some_module }
      - { module: another_module }
      - { module: insecure_module, action: secure_action }
      - { module: insecure_module, action: another_secure_action }

In the previous code, the

  strict: true

code forces strict checking. Strict checking means that if someone accesses a URL that isn’t specified as secure via HTTPS, it will redirect them to the HTTP. If strict is set to false, it will allow them to access any module through HTTPS.

After that, the

      - { module: insecure_module, action: another_secure_action }

code is how you set each module and action. If you want an entire module to be secure, don’t include the action section of that, however you need a line like that for every module and action you want to secure.

Create a Filter

Create a file in apps/app_name/lib/ named sfSslFilter.php, and put in it:

<?php
/**
 * @author Graham Christensen
 * 			graham dot christensen at iamgraham dot net
 */
class sslFilter extends sfFilter {
	public function execute ($filterChain) {
		$context = $this->getContext();
		$request = $context->getRequest();

		// Perform strict checking of security
		// IE: If it's HTTPS and shouldn't be, make it HTTP
		if (sfConfig::has('app_ssl_strict')) {
			$only_explicit = (bool)
							 sfConfig::get('app_ssl_strict');
		} else {
			$only_explicit = false;
		}

		// Get a list of all the modules to check for
		$modules = sfConfig::get('app_ssl_modules');

		// Set the modules variable to an array, this is
		// if it's not configured for this particular environment.
		if (!is_array($modules)) {
			$modules = array();
		}

		// Store the module name and action name into variables
		// to simplify the code, and reduce function calls.
		$module_name = $context->getModuleName();
		$action_name = $context->getActionName();

		// Check if the current request matches a security module
		// If the module or module & action is specified, then
		// ensure it's correctly set.
		$listed = false;
		foreach ($modules as $action) {
			// If the module name is listed
			if ($action['module'] == $module_name) {
				// If the whole module is listed, or the action
				// specifically
				if (!isset($action['action'])
					|| $action_name == $action['action']) {
					$listed = true;
					break;
				}
			}
		}

		$is_secure = $request->isSecure();

		// If modules have to be explicitly listed, it is
		// secure, and it's not listed - then redirect
		if ($only_explicit && $is_secure && !$listed) {
			return self::doRedirect($context);
		}

		// If it's not secure, and it's listed as having to be
		if (!$is_secure && $listed) {
			return self::doRedirect($context);
		}

		// Continue on with the chain, but it will only do that if
		// we didn't need to redirect.
		$filterChain->execute();
	}

	public static function doRedirect($context) {
		$request = $context->getRequest();
		$controller = $context->getController();

		// Determine which direction we want to go
		if ($request->isSecure()) {
			// Switch to insecure
			$from = 'https://';
			$to   = 'http://';
		} else {
			// Switch to secure
			$from = 'http://';
			$to   = 'https://';
		}

		$redirect_to = str_replace($from, $to, $request->getUri());
		return $controller->redirect($redirect_to, 0, 301);
	}
}

The beginning of this is pretty simple, it just gets the configuration settings outlined in the app.yml file, and then goes to town doing it’s job.

		// Check if the current request matches a security module
		// If the module or module & action is specified, then
		// ensure it's correctly set.
		$listed = false;
		foreach ($modules as $action) {
			// If the module name is listed
			if ($action['module'] == $module_name) {
				// If the whole module is listed, or the action
				// specifically
				if (!isset($action['action'])
					|| $action_name == $action['action']) {
					$listed = true;
					break;
				}
			}
		}

That code goes over every module in the app.yml file, and sees if the current requests’ module matches, or if the module and action matches. If it does, it sets $listed to true and exits the loop. This tells the code that it is needs to ensure security on the current request.

		// If modules have to be explicitly listed, it is
		// secure, and it's not listed - then redirect
		if ($only_explicit && $is_secure && !$listed) {
			return self::doRedirect($context);
		}

That code ensures that if explicit checking is on and that the request is currently using HTTPS and it shouldn’t be, it is redirected to become HTTP.

		// If it's not secure, and it's listed as having to be
		if (!$is_secure && $listed) {
			return self::doRedirect($context);
		}

If the module/action is listed as needing to be secure and it isn’t, then perform the redirect.

	public static function doRedirect($context) {
		$request = $context->getRequest();
		$controller = $context->getController();

		// Determine which direction we want to go
		if ($request->isSecure()) {
			// Switch to insecure
			$from = 'https://';
			$to   = 'http://';
		} else {
			// Switch to secure
			$from = 'http://';
			$to   = 'https://';
		}

		$redirect_to = str_replace($from, $to, $request->getUri());
		return $controller->redirect($redirect_to, 0, 301);
	}

The previous method is really pretty simple. If doRequest is called, it means that the current protocol is not valid. So, if it is HTTP, make it HTTPS. If it’s HTTPS, make it HTTP.

Finishing Up

When you’re finishing up, make sure you clear your cache via

./symfony cc

and then test it on your site. Make sure that your strict option is working, and you should check each individual module and action to make sure you didn’t forget anything.

  • @Ruddy: I experienced this as well and would suggest to the author to expand this filter to accomodate per-module "strict" options.

    In the mean time, I made a small change to the filter to make an exception for my 404 module/action combination:

    This:
    if ($only_explicit && $is_secure && !$listed) {

    Becomes this:
    if ($only_explicit && $is_secure && !$listed) && !($module_name == 'my404module' && $action_name == 'my404action')) {
  • Ruddy
    This stuff can cause an infinite loop if :
    - you use forward404 in a secure action
    - you overwrite your 404 action to home/error404 for example which is not secure...

    We really need to have the strict option there but have to be able to setup a strict option per module also instead of a global setting.
    So, I can have strict=false for home/error404 which needs to be display in secure and non-secure fashion
  • I am facing the same task currently and this post helps me a lot (and also Jesteps). But I'm wondering whether there's a automatic way to include additional behaviour to the provided redirection solution (which is fine for directly typed links and so on). Specifically I really would like to have urls generated with the url_for, link_to helper group to automatically reference the https protocol depending on the given application configuration.

    I read about hacks been applied to older versions of symfony achieving this, but I guess there has to be a proper way of overloading gen_url.

    This way the mentioned problem regarding post requests (on login forms for example) wouldn't occur, given that the "action" attribute was generated with one of the said methods.

    Best regards
  • One thing that I didn't address in my script, and it looks like isn't covered in yours either, was if POST requests are redirected. Since the post variables wont get forwarded to the secure version of the page, it's possible to cause some major usability problems.
  • Unfortunately, as far as I know it isn't possible to re-send the POST information. Since it is a POST request and not a GET request, they can't be passed through the URL. Also, the sfSslRequirementPlugin for Symfony, also didn't implement this feature. as far as I can tell. Although, if you find a way to address that issue - please let me know.

    Good catch, I hadn't even thought about that.

    Graham
blog comments powered by Disqus