possible csrf attack error

Security token doesn't match, possible CSRF attack

When a contact form is submitted after the PHP session has expired (or if cookies aren't enabled at all), instead of showing a 400 page, the user is confronted with a whitescreen shouting 'Security token doesn't match, possible CSRF attack.'. There ar two things I would like to be different:

  1. Show a nice 400 page instead
  2. Autogenerate the SilverStripe 400 errorpage on install (RequireDefaultRecords)

The culprit

/sapphire/forms/Form.php line 236:

if(!$token->checkRequest($request))
	$this->httpError(400, "Security token doesn't match, possible CSRF attack.");

httpError() explained
There are two httpError() functions in SilverStripe. One in the RequestHandler class and one in the ContentController class. Of the two, the RequestHandler just throws the whitescreen erromessage, the ContentController will return an actual errorpage. Since the form extends the RequestHandler, but not the ContentController, $this->httpError() uses the whitescreen version. If the Form's controller is a page (it usually is) this can easily be hacked by doing $this->controller->httpError() instead.

So - what do you do?

Step 1 - create a 400 errorpage

If you want the 400 errorpage to show, you have to create and publish one in the CMS! Unfortunately, this (static) page has no way to show a custom message - it shows what you type in the content area, and that's it...

Step 2: extend the Form class with a new httpError() method

The clean way to handle the problem is to extend the Form and override its (parents) httpError() method.

/mysite/code/MyForm.php:

class MyForm extends Form {

   ...

	public function httpError($code, $message = null) {
		if (!(Permission::check("ADMIN"))) {
			$response = ErrorPage::response_for($code);
		}
		if (empty($response)) $response = $message;
		throw new SS_HTTPResponse_Exception($response);
	}
}

This will show the original error message if you're logged in as administrator, and the 400 errorpage if you're not (including when cookies are disabled). If you're still somewhat unfamiliar with forms: you now can create a new MyForm( ... ) instead of a new Form( ... ) whenever you need one.

Autogenerate a 400 errorpage

While we're at it - why not autogenerate a 400 errorpage on install, just like the 404 and the 500 error page in SilverStripe:

In your Page class:

function requireDefaultRecords() {

	parent::requireDefaultRecords();

	// create a 400 ErrorPage
	if ($this->class == 'ErrorPage') {

		// Ensure that an assets path exists before we do any error page creation
		if(!file_exists(ASSETS_PATH)) {
			mkdir(ASSETS_PATH);
		}
		
		$ErrorPage400 = DataObject::get_one('ErrorPage', "\"ErrorCode\" = '400'");
		$ErrorPage400Exists = ($ErrorPage400 && $ErrorPage400->exists()) ? true : false;
		$ErrorPage400Path = ErrorPage::get_filepath_for_errorcode(400);
		if(!($ErrorPage400Exists && file_exists($ErrorPage400Path))) {
			if(!$ErrorPage400Exists) {
				$ErrorPage400 = new ErrorPage();
				$ErrorPage400->ErrorCode = 400;
				$ErrorPage400->Title = _t('ErrorPage.ERRORPAGE400TITLE', '400 Error');
				$ErrorPage400->Content = _t(
					'ErrorPage.ERRORPAGE400CONTENT', 
					'<p>An error occurred while processing your request.</p>'
				);
				$ErrorPage400->Status = 'New page';
				$ErrorPage400->write();
				$ErrorPage400->publish('Stage', 'Live');
			}

			// Ensure a static error page is created from latest error page content
			$response = Director::test(Director::makeRelative($ErrorPage400->Link()));
			if($fh = fopen($ErrorPage400Path, 'w')) {
				$written = fwrite($fh, $response->getBody());
				fclose($fh);
			}

			if($written) {
				DB::alteration_message('400 error page created', 'created');
			} else {
				DB::alteration_message(sprintf(
					'400 error page could not be created at %s. Please check permissions', 
					$ErrorPage400Path), 'error');
			}
		}			
	}
}

@TODO: Don't show the form when cookies are disabled

To do this, you need a way to check if the Session is actually set. Think about it...

@TODO: What about the UserForms?

Unfortunately this will not work out of the box for the UserForms, since this module extends the Form class directly. Using a Form Decorator is also not an option, since the httpError() method already exists in the Form (parent)class. So you probably have to hack the userforms and have them either extend MyForm, or add the httpError() method... Or, if you don't want that, just hack the method into the Form class itself and be done with it :-(