silverstripe-framework/src/Security/SecurityToken.php

291 lines
6.8 KiB
PHP

<?php
namespace SilverStripe\Security;
use SilverStripe\Control\Controller;
use SilverStripe\Control\HTTPRequest;
use SilverStripe\Control\Session;
use SilverStripe\Core\Config\Configurable;
use SilverStripe\Core\Injector\Injectable;
use SilverStripe\Forms\FieldList;
use SilverStripe\Forms\HiddenField;
use SilverStripe\View\TemplateGlobalProvider;
/**
* Cross Site Request Forgery (CSRF) protection for the {@link Form} class and other GET links.
* Can be used globally (through {@link SecurityToken::inst()})
* or on a form-by-form basis {@link Form->getSecurityToken()}.
*
* <b>Usage in forms</b>
*
* This protective measure is automatically turned on for all new {@link Form} instances,
* and can be globally disabled through {@link disable()}.
*
* <b>Usage in custom controller actions</b>
*
* <code>
* class MyController extends Controller {
* function mygetaction($request) {
* if(!SecurityToken::inst()->checkRequest($request)) return $this->httpError(400);
*
* // valid action logic ...
* }
* }
* </code>
*
* @todo Make token name form specific for additional forgery protection.
*/
class SecurityToken implements TemplateGlobalProvider
{
use Configurable;
use Injectable;
/**
* @var string
*/
protected static $default_name = 'SecurityID';
/**
* @var SecurityToken
*/
protected static $inst = null;
/**
* @var boolean
*/
protected static $enabled = true;
/**
* @var String $name
*/
protected $name = null;
/**
* @param string $name
*/
public function __construct($name = null)
{
$this->name = $name ?: self::get_default_name();
}
/**
* Gets a global token (or creates one if it doesnt exist already).
*
* @return SecurityToken
*/
public static function inst()
{
if (!self::$inst) {
self::$inst = new SecurityToken();
}
return self::$inst;
}
/**
* Globally disable the token (override with {@link NullSecurityToken})
* implementation. Note: Does not apply for
*/
public static function disable()
{
self::$enabled = false;
self::$inst = new NullSecurityToken();
}
/**
* Globally enable tokens that have been previously disabled through {@link disable}.
*/
public static function enable()
{
self::$enabled = true;
self::$inst = new SecurityToken();
}
/**
* @return boolean
*/
public static function is_enabled()
{
return self::$enabled;
}
/**
* @return String
*/
public static function get_default_name()
{
return self::$default_name;
}
/**
* Returns the value of an the global SecurityToken in the current session
* @return int
*/
public static function getSecurityID()
{
$token = SecurityToken::inst();
return $token->getValue();
}
/**
* @param string $name
*/
public function setName($name)
{
$val = $this->getValue();
$this->name = $name;
$this->setValue($val);
}
/**
* @return string
*/
public function getName()
{
return $this->name;
}
/**
* @return String
*/
public function getValue()
{
$session = Controller::curr()->getRequest()->getSession();
$value = $session->get($this->getName());
// only regenerate if the token isn't already set in the session
if (!$value) {
$value = $this->generate();
$this->setValue($value);
}
return $value;
}
/**
* @param String $val
*/
public function setValue($val)
{
$session = Controller::curr()->getRequest()->getSession();
$session->set($this->getName(), $val);
}
/**
* Reset the token to a new value.
*/
public function reset()
{
$this->setValue($this->generate());
}
/**
* Checks for an existing CSRF token in the current users session.
* This check is automatically performed in {@link Form->httpSubmission()}
* if a form has security tokens enabled.
* This direct check is mainly used for URL actions on {@link FormField} that are not routed
* through {@link Form->httpSubmission()}.
*
* Typically you'll want to check {@link Form->securityTokenEnabled()} before calling this method.
*
* @param String $compare
* @return Boolean
*/
public function check($compare)
{
return ($compare && $this->getValue() && $compare == $this->getValue());
}
/**
* See {@link check()}.
*
* @param HTTPRequest $request
* @return bool
*/
public function checkRequest($request)
{
$token = $this->getRequestToken($request);
return $this->check($token);
}
/**
* Get security token from request
*
* @param HTTPRequest $request
* @return string
*/
protected function getRequestToken($request)
{
$name = $this->getName();
$header = 'X-' . ucwords(strtolower($name));
if ($token = $request->getHeader($header)) {
return $token;
}
// Get from request var
return $request->requestVar($name);
}
/**
* Note: Doesn't call {@link FormField->setForm()}
* on the returned {@link HiddenField}, you'll need to take
* care of this yourself.
*
* @param FieldList $fieldset
* @return HiddenField|false
*/
public function updateFieldSet(&$fieldset)
{
if (!$fieldset->fieldByName($this->getName())) {
$field = new HiddenField($this->getName(), null, $this->getValue());
$fieldset->push($field);
return $field;
} else {
return false;
}
}
/**
* @param String $url
* @return String
*/
public function addToUrl($url)
{
return Controller::join_links($url, sprintf('?%s=%s', $this->getName(), $this->getValue()));
}
/**
* You can't disable an existing instance, it will need to be overwritten like this:
* <code>
* $old = SecurityToken::inst(); // isEnabled() returns true
* SecurityToken::disable();
* $new = SecurityToken::inst(); // isEnabled() returns false
* </code>
*
* @return boolean
*/
public function isEnabled()
{
return !($this instanceof NullSecurityToken);
}
/**
* @uses RandomGenerator
*
* @return String
*/
protected function generate()
{
$generator = new RandomGenerator();
return $generator->randomToken('sha1');
}
public static function get_template_global_variables()
{
return array(
'getSecurityID',
'SecurityID' => 'getSecurityID'
);
}
}