Putting the “Secure” in Secure Mode Forms

The Secure Mode setting in ExpressionEngine serves a very important function and is often misunderstood. Secure Mode ensures that all forms processed by ExpressionEngine came from the visitor who was given the form and not some other source that may be trying to do very bad things. If the call is not coming from the visitor it was generated for, ExpressionEngine will reject it.

At its most basic level, this feature helps prevent spam bots from bombing your site with automated data as they try to add unwelcome viagra ads to your innocent comments section. However, the really important duty of Secure Mode is stopping cross-site request forgery attacks which include not only common spammers but also malicious external attacks.

Cross-Site Request Forgery (CSRF)

Let’s take a look at how cross-site request forgery works. Say I have a book review site that relies heavily on member input. Members are allowed to rate books, and higher rated books earn positions of prominence on the site.

The site uses a simple ratings module that provides logged-in members with a list of rating links for each book. When they click the link, my module grabs the rating, checks the member ID and either inserts or updates their rating for the book. The HTML output from my module for rating books might look something like this:

<ul class="rating-me">
    <
li><a href="http://example.com?ACT=20&id=50&rating=1">1</a></li>
    <
li><a href="http://example.com?ACT=20&id=50&rating=2">2</a></li>
    <
li><a href="http://example.com?ACT=20&id=50&rating=3">3</a></li>
    <
li><a href="http://example.com?ACT=20&id=50&rating=4">4</a></li>
    <
li><a href="http://example.com?ACT=20&id=50&rating=5">5</a></li>
</
ul

This works brilliantly for quite some time until I notice that some really horrid books in a particularly putrid series are suddenly receiving a major influx of ‘5’ star ratings. Investigating, I find that clever fans of the series are posting disguised links on another fan site’s forums. My less savvy users click a link for ‘Vote for more westerns’, get a ‘thank you’ redirect on my site, and never realize they’ve just given the latest vampire-zombie romance novel a 5 star rating!

That is a classic cross-site request forgery and a pretty major flaw in my rating script. To patch my security hole, I’m going to switch from using a link to rate books to using a form to post ratings.

As long as I have Secure Mode turned on, this will slam the lid on my CSRF vulnerability. With Secure Mode turned off, there’s nothing to prevent fraudulent form data from another site from being processed. Particularly crafty attackers can even silently carry out this action by enticing my members to visit their own site at which point they submit whatever they want with JavaScript, and my site’s members who are logged in will be posting ratings to my site without their even knowing. And with my users, all it takes is the suggestion of a cute cat GIF and away they go.

It’s my job to make sure they don’t get taken advantage of. While up voting a bad novel may seem inconsequential, a successful attack carried out on a logged in super admin could forge sales, membership data, or even delete content. Turning Secure Mode on helps ensure that such fraudulent posted data will be rejected.

How It Works

How does Secure Mode work? It’s pretty simple. Every time a page with a form on it loads, a new secure form ID (XID) is created. That ID is recorded in the database along with the date and the session ID of the user who loaded the page.

Every form should send the XID variable containing that value as part of the submitted data. Every single post is then checked and if there is no matching XID in the database, the form is rejected.

Troubleshooting Secure Mode

So what happens when good XIDs go bad? On the front end, if you submit a form with an invalid XID it will generate a system error message:

In the control panel, it will result in a redirect to the control panel homepage.

The logic behind Secure Mode is simple and there are very few places where errors could occur. However, If you think forms are being rejected when they shouldn’t be, there are several steps you can take to get to the bottom it.

  1. If you can consistently trigger the behavior, the quickest way to test whether it’s due to Secure Mode is to turn Secure Mode off (in the control panel under Admin ‣ Security and Privacy ‣ Security and Sessions). Leaving Secure Mode off is obviously not recommended. But this allows you to quickly determine if Secure Mode is indeed at issue.
  2. Make sure you are running the latest version of ExpressionEngine. As of 2.7.0, significant improvements to Secure Mode have been made to be more automated and give developers more control over what to do in the case of a bad XID (see Security::restore_xid(), for example). Developers who take advantage of this enable site administrators to leave Secure Mode enabled, but still accept POST data from web services like Stripe, GitHub, etc. that will submit data without a valid ExpressionEngine XID.
  3. Ensure your server time is set correctly and check your ExpressionEngine server timezone setting. Both the form hash ID and the session ID are time sensitive. If ExpressionEngine is calculating when to expire them based on an incorrect time, your hash ID may expire much sooner than it should.

Creating Forms that are Secured

For most ExpressioneEngine site designers, you’ll never have to worry about Secure Mode at all. Turn it on, leave it on, it should just work.

Developers are far more likely to need to account for Secure Mode in their add-ons. Any form you create must send an XID value or it will fail with Secure Mode turned on. Fortunately, the setting and checking of the XID hash is largely automatic as long as a developer is using best practices.

In some very rare cases, you may have a form that has not had the XID added. For example, sometimes I’ll toss up a hard coded HTML form in a template for quick and dirty testing:

<?php print_r($_POST); ?>

<form method="post" action="">
    <
input type="text" name="post_test" value="">
    <
input type="submit" value="submit">
</
form

This will not work as it has no XID and will be rejected as insecure.

If you run into one of these edge cases, you can easily add the XID in manually using the {XID_HASH} global variable:

<?php print_r($_POST); ?>

<form method="post" action="">
    <
input type="hidden" name="XID" value="{XID_HASH}">    
    <
input type="text" name="post_test" value="">
    <
input type="submit" value="submit">
</
form

The form now works and your form processing is no longer vulnerable to cross-site request forgery.

Secure Mode - The Unsung Hero

ExpressionEngine is designed to provide a high level of security by default and Secure Mode is one of the tools we use to make that happen. As with many security features, you probably never noticed it’s there. That’s the nature of security, you tend to only notice it when it fails.

You may be wondering why it’s even an option if it’s so important. In ExpressionEngine 1, CSRF protection was tied to the visitor’s IP address, and it caused many problems for people on service providers that rapidly rotate IP addresses like AOL, as well as people behind corporate or country firewalls (South Africa comes to mind) where all site visitors might be sharing the same IP address. Throughout ExpressionEngine 2’s life cycle, we’ve been making iterative improvements that will allow us the option to remove that setting in the future. One big element was released in 2.7.0, and we’ve been providing assistance to third-party developers to help them work XIDs into their add-ons so that if and when we make such a change in a future release, functionality on your site doesn’t quit working.

Hopefully this explanation has helped you understand what this setting is in ExpressionEngine and how it helps protect your site and site visitors from one common internet attack. If this topic was interesting to you, here is some recommended reading:

.(JavaScript must be enabled to view this email address) or share your feedback on this entry with @ellislab on Twitter.