mirror of
https://github.com/silverstripe/silverstripe-framework
synced 2024-10-22 14:05:37 +02:00
289 lines
6.7 KiB
PHP
289 lines
6.7 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()
|
|
{
|
|
$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::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'
|
|
);
|
|
}
|
|
}
|