[Security] Added CSRF sub-component

This commit is contained in:
Bernhard Schussek 2013-09-27 09:05:52 +02:00
parent e24dbf7cba
commit 1bf16400fb
15 changed files with 893 additions and 0 deletions

View File

@ -52,6 +52,7 @@
"symfony/security": "self.version",
"symfony/security-acl": "self.version",
"symfony/security-core": "self.version",
"symfony/security-csrf": "self.version",
"symfony/security-http": "self.version",
"symfony/security-bundle": "self.version",
"symfony/serializer": "self.version",

View File

@ -0,0 +1,3 @@
vendor/
composer.lock
phpunit.xml

View File

@ -0,0 +1,105 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Security\Csrf;
use Symfony\Component\Security\Core\Util\SecureRandomInterface;
use Symfony\Component\Security\Core\Util\SecureRandom;
use Symfony\Component\Security\Core\Util\StringUtils;
use Symfony\Component\Security\Csrf\TokenStorage\NativeSessionTokenStorage;
use Symfony\Component\Security\Csrf\TokenStorage\TokenStorageInterface;
/**
* Generates and validates CSRF tokens.
*
* @since 2.4
* @author Bernhard Schussek <bernhard.schussek@symfony.com>
*/
class CsrfTokenGenerator implements CsrfTokenGeneratorInterface
{
/**
* The entropy of the token in bits.
* @var integer
*/
const TOKEN_ENTROPY = 256;
/**
* @var TokenStorageInterface
*/
private $storage;
/**
* The generator for random values.
* @var SecureRandomInterface
*/
private $random;
/**
* Creates a new CSRF provider using PHP's native session storage.
*
* @param TokenStorageInterface $storage The storage for storing generated
* CSRF tokens
* @param SecureRandomInterface $random The used random value generator
* @param integer $entropy The amount of entropy collected for
* newly generated tokens (in bits)
*
*/
public function __construct(TokenStorageInterface $storage = null, SecureRandomInterface $random = null, $entropy = self::TOKEN_ENTROPY)
{
if (null === $storage) {
$storage = new NativeSessionTokenStorage();
}
if (null === $random) {
$random = new SecureRandom();
}
$this->storage = $storage;
$this->random = $random;
$this->entropy = $entropy;
}
/**
* {@inheritDoc}
*/
public function generateCsrfToken($tokenId)
{
$currentToken = $this->storage->getToken($tokenId, false);
// Token exists and is still valid
if (false !== $currentToken) {
return $currentToken;
}
// Token needs to be (re)generated
// Generate an URI safe base64 encoded string that does not contain "+",
// "/" or "=" which need to be URL encoded and make URLs unnecessarily
// longer.
$bytes = $this->random->nextBytes($this->entropy / 8);
$token = rtrim(strtr(base64_encode($bytes), '+/', '-_'), '=');
$this->storage->setToken($tokenId, $token);
return $token;
}
/**
* {@inheritDoc}
*/
public function isCsrfTokenValid($tokenId, $token)
{
if (!$this->storage->hasToken($tokenId)) {
return false;
}
return StringUtils::equals((string) $this->storage->getToken($tokenId), $token);
}
}

View File

@ -0,0 +1,50 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Security\Csrf;
/**
* Generates and validates CSRF tokens.
*
* You can generate a CSRF token by using the method {@link generateCsrfToken()}.
* This method expects a unique token ID as argument. The token ID can later be
* used to validate a token provided by the user.
*
* Token IDs do not necessarily have to be secret, but they should NEVER be
* created from data provided by the client. A good practice is to hard-code the
* token IDs for the various CSRF tokens used by your application.
*
* You should use the method {@link isCsrfTokenValid()} to check a CSRF token
* submitted by the client. This method will return true if the CSRF token is
* valid.
*
* @since 2.4
* @author Bernhard Schussek <bschussek@gmail.com>
*/
interface CsrfTokenGeneratorInterface
{
/**
* Generates a CSRF token with the given token ID.
*
* @param string $tokenId An ID that identifies the token
*/
public function generateCsrfToken($tokenId);
/**
* Validates a CSRF token.
*
* @param string $tokenId The token ID used when generating the token
* @param string $token The token supplied by the client
*
* @return Boolean Whether the token supplied by the client is correct
*/
public function isCsrfTokenValid($tokenId, $token);
}

View File

@ -0,0 +1,19 @@
Copyright (c) 2004-2013 Fabien Potencier
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is furnished
to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.

View File

@ -0,0 +1,21 @@
Security Component - CSRF
=========================
The Security CSRF (cross-site request forgery) component provides a class
`CsrfTokenGenerator` for generating and validating CSRF tokens.
Resources
---------
Documentation:
http://symfony.com/doc/2.4/book/security.html
Tests
-----
You can run the unit tests with the following command:
$ cd path/to/Symfony/Component/Security/Csrf/
$ composer.phar install --dev
$ phpunit

View File

@ -0,0 +1,148 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Form\Tests\Extension\Csrf\CsrfProvider;
use Symfony\Component\Security\Csrf\CsrfTokenGenerator;
/**
* @author Bernhard Schussek <bschussek@gmail.com>
*/
class CsrfTokenGeneratorTest extends \PHPUnit_Framework_TestCase
{
/**
* A non alpha-numeric byte string
* @var string
*/
private static $bytes;
/**
* @var \PHPUnit_Framework_MockObject_MockObject
*/
private $random;
/**
* @var \PHPUnit_Framework_MockObject_MockObject
*/
private $storage;
/**
* @var CsrfTokenGenerator
*/
private $generator;
public static function setUpBeforeClass()
{
self::$bytes = base64_decode('aMf+Tct/RLn2WQ==');
}
protected function setUp()
{
$this->random = $this->getMock('Symfony\Component\Security\Core\Util\SecureRandomInterface');
$this->storage = $this->getMock('Symfony\Component\Security\Csrf\TokenStorage\TokenStorageInterface');
$this->generator = new CsrfTokenGenerator($this->storage, $this->random);
}
protected function tearDown()
{
$this->random = null;
$this->storage = null;
$this->generator = null;
}
public function testGenerateNewToken()
{
$this->storage->expects($this->once())
->method('getToken')
->with('token_id', false)
->will($this->returnValue(false));
$this->storage->expects($this->once())
->method('setToken')
->with('token_id', $this->anything())
->will($this->returnCallback(function ($tokenId, $token) use (&$storedToken) {
$storedToken = $token;
}));
$this->random->expects($this->once())
->method('nextBytes')
->will($this->returnValue(self::$bytes));
$token = $this->generator->generateCsrfToken('token_id');
$this->assertSame($token, $storedToken);
$this->assertTrue(ctype_print($token), 'is printable');
$this->assertStringNotMatchesFormat('%S+%S', $token, 'is URI safe');
$this->assertStringNotMatchesFormat('%S/%S', $token, 'is URI safe');
$this->assertStringNotMatchesFormat('%S=%S', $token, 'is URI safe');
}
public function testUseExistingTokenIfAvailable()
{
$this->storage->expects($this->once())
->method('getToken')
->with('token_id', false)
->will($this->returnValue('TOKEN'));
$this->storage->expects($this->never())
->method('setToken');
$this->random->expects($this->never())
->method('nextBytes');
$token = $this->generator->generateCsrfToken('token_id');
$this->assertEquals('TOKEN', $token);
}
public function testMatchingTokenIsValid()
{
$this->storage->expects($this->once())
->method('hasToken')
->with('token_id')
->will($this->returnValue(true));
$this->storage->expects($this->once())
->method('getToken')
->with('token_id')
->will($this->returnValue('TOKEN'));
$this->assertTrue($this->generator->isCsrfTokenValid('token_id', 'TOKEN'));
}
public function testNonMatchingTokenIsNotValid()
{
$this->storage->expects($this->once())
->method('hasToken')
->with('token_id')
->will($this->returnValue(true));
$this->storage->expects($this->once())
->method('getToken')
->with('token_id')
->will($this->returnValue('TOKEN'));
$this->assertFalse($this->generator->isCsrfTokenValid('token_id', 'FOOBAR'));
}
public function testNonExistingTokenIsNotValid()
{
$this->storage->expects($this->once())
->method('hasToken')
->with('token_id')
->will($this->returnValue(false));
$this->storage->expects($this->never())
->method('getToken');
$this->assertFalse($this->generator->isCsrfTokenValid('token_id', 'FOOBAR'));
}
}

View File

@ -0,0 +1,99 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Form\Tests\Extension\Csrf\CsrfProvider;
use Symfony\Component\Security\Csrf\TokenStorage\NativeSessionTokenStorage;
/**
* @author Bernhard Schussek <bschussek@gmail.com>
*
* @runTestsInSeparateProcesses
*/
class NativeSessionTokenStorageTest extends \PHPUnit_Framework_TestCase
{
const SESSION_NAMESPACE = 'foobar';
/**
* @var NativeSessionTokenStorage
*/
private $storage;
public static function setUpBeforeClass()
{
ini_set('session.save_handler', 'files');
ini_set('session.save_path', sys_get_temp_dir());
parent::setUpBeforeClass();
}
protected function setUp()
{
$_SESSION = array();
$this->storage = new NativeSessionTokenStorage(self::SESSION_NAMESPACE);
}
public function testStoreTokenInClosedSession()
{
$this->storage->setToken('token_id', 'TOKEN');
$this->assertSame(array(self::SESSION_NAMESPACE => array('token_id' => 'TOKEN')), $_SESSION);
}
public function testStoreTokenInClosedSessionWithExistingSessionId()
{
session_id('foobar');
$this->assertSame(PHP_SESSION_NONE, session_status());
$this->storage->setToken('token_id', 'TOKEN');
$this->assertSame(PHP_SESSION_ACTIVE, session_status());
$this->assertSame(array(self::SESSION_NAMESPACE => array('token_id' => 'TOKEN')), $_SESSION);
}
public function testStoreTokenInActiveSession()
{
session_start();
$this->storage->setToken('token_id', 'TOKEN');
$this->assertSame(array(self::SESSION_NAMESPACE => array('token_id' => 'TOKEN')), $_SESSION);
}
/**
* @depends testStoreTokenInClosedSession
*/
public function testCheckToken()
{
$this->assertFalse($this->storage->hasToken('token_id'));
$this->storage->setToken('token_id', 'TOKEN');
$this->assertTrue($this->storage->hasToken('token_id'));
}
/**
* @depends testStoreTokenInClosedSession
*/
public function testGetExistingToken()
{
$this->storage->setToken('token_id', 'TOKEN');
$this->assertSame('TOKEN', $this->storage->getToken('token_id'));
}
public function testGetNonExistingToken()
{
$this->assertSame('DEFAULT', $this->storage->getToken('token_id', 'DEFAULT'));
}
}

View File

@ -0,0 +1,144 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Form\Tests\Extension\Csrf\CsrfProvider;
use Symfony\Component\Security\Csrf\TokenStorage\SessionTokenStorage;
/**
* @author Bernhard Schussek <bschussek@gmail.com>
*/
class SessionTokenStorageTest extends \PHPUnit_Framework_TestCase
{
const SESSION_NAMESPACE = 'foobar';
/**
* @var \PHPUnit_Framework_MockObject_MockObject
*/
private $session;
/**
* @var SessionTokenStorage
*/
private $storage;
protected function setUp()
{
if (!class_exists('Symfony\Component\HttpFoundation\Session\SessionInterface')) {
$this->markTestSkipped('The "HttpFoundation" component is not available');
}
$this->session = $this->getMockBuilder('Symfony\Component\HttpFoundation\Session\SessionInterface')
->disableOriginalConstructor()
->getMock();
$this->storage = new SessionTokenStorage($this->session, self::SESSION_NAMESPACE);
}
public function testStoreTokenInClosedSession()
{
$this->session->expects($this->any())
->method('isStarted')
->will($this->returnValue(false));
$this->session->expects($this->once())
->method('start');
$this->session->expects($this->once())
->method('set')
->with(self::SESSION_NAMESPACE.'/token_id', 'TOKEN');
$this->storage->setToken('token_id', 'TOKEN');
}
public function testStoreTokenInActiveSession()
{
$this->session->expects($this->any())
->method('isStarted')
->will($this->returnValue(true));
$this->session->expects($this->never())
->method('start');
$this->session->expects($this->once())
->method('set')
->with(self::SESSION_NAMESPACE.'/token_id', 'TOKEN');
$this->storage->setToken('token_id', 'TOKEN');
}
public function testCheckTokenInClosedSession()
{
$this->session->expects($this->any())
->method('isStarted')
->will($this->returnValue(false));
$this->session->expects($this->once())
->method('start');
$this->session->expects($this->once())
->method('has')
->with(self::SESSION_NAMESPACE.'/token_id')
->will($this->returnValue('RESULT'));
$this->assertSame('RESULT', $this->storage->hasToken('token_id'));
}
public function testCheckTokenInActiveSession()
{
$this->session->expects($this->any())
->method('isStarted')
->will($this->returnValue(true));
$this->session->expects($this->never())
->method('start');
$this->session->expects($this->once())
->method('has')
->with(self::SESSION_NAMESPACE.'/token_id')
->will($this->returnValue('RESULT'));
$this->assertSame('RESULT', $this->storage->hasToken('token_id'));
}
public function testGetTokenFromClosedSession()
{
$this->session->expects($this->any())
->method('isStarted')
->will($this->returnValue(false));
$this->session->expects($this->once())
->method('start');
$this->session->expects($this->once())
->method('get')
->with(self::SESSION_NAMESPACE.'/token_id', 'DEFAULT')
->will($this->returnValue('RESULT'));
$this->assertSame('RESULT', $this->storage->getToken('token_id', 'DEFAULT'));
}
public function testGetTokenFromActiveSession()
{
$this->session->expects($this->any())
->method('isStarted')
->will($this->returnValue(true));
$this->session->expects($this->never())
->method('start');
$this->session->expects($this->once())
->method('get')
->with(self::SESSION_NAMESPACE.'/token_id', 'DEFAULT')
->will($this->returnValue('RESULT'));
$this->assertSame('RESULT', $this->storage->getToken('token_id', 'DEFAULT'));
}
}

View File

@ -0,0 +1,101 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Security\Csrf\TokenStorage;
/**
* Token storage that uses PHP's native session handling.
*
* @since 2.4
* @author Bernhard Schussek <bschussek@gmail.com>
*/
class NativeSessionTokenStorage implements TokenStorageInterface
{
/**
* The namespace used to store values in the session.
* @var string
*/
const SESSION_NAMESPACE = '_csrf';
/**
* @var Boolean
*/
private $sessionStarted = false;
/**
* @var string
*/
private $namespace;
/**
* Initializes the storage with a session namespace.
*
* @param string $namespace The namespace under which the token is stored
* in the session
*/
public function __construct($namespace = self::SESSION_NAMESPACE)
{
$this->namespace = $namespace;
}
/**
* {@inheritdoc}
*/
public function getToken($tokenId, $default = null)
{
if (!$this->sessionStarted) {
$this->startSession();
}
if (isset($_SESSION[$this->namespace][$tokenId])) {
return $_SESSION[$this->namespace][$tokenId];
}
return $default;
}
/**
* {@inheritdoc}
*/
public function setToken($tokenId, $token)
{
if (!$this->sessionStarted) {
$this->startSession();
}
$_SESSION[$this->namespace][$tokenId] = $token;
}
/**
* {@inheritdoc}
*/
public function hasToken($tokenId)
{
if (!$this->sessionStarted) {
$this->startSession();
}
return isset($_SESSION[$this->namespace][$tokenId]);
}
private function startSession()
{
if (version_compare(PHP_VERSION, '5.4', '>=')) {
if (PHP_SESSION_NONE === session_status()) {
session_start();
}
} elseif (!session_id()) {
session_start();
}
$this->sessionStarted = true;
}
}

View File

@ -0,0 +1,89 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Security\Csrf\TokenStorage;
use Symfony\Component\HttpFoundation\Session\SessionInterface;
/**
* Token storage that uses a Symfony2 Session object.
*
* @since 2.4
* @author Bernhard Schussek <bschussek@gmail.com>
*/
class SessionTokenStorage implements TokenStorageInterface
{
/**
* The namespace used to store values in the session.
* @var string
*/
const SESSION_NAMESPACE = '_csrf';
/**
* The user session from which the session ID is returned
* @var SessionInterface
*/
private $session;
/**
* @var string
*/
private $namespace;
/**
* Initializes the storage with a Session object and a session namespace.
*
* @param SessionInterface $session The user session
* @param string $namespace The namespace under which the token
* is stored in the session
*/
public function __construct(SessionInterface $session, $namespace = self::SESSION_NAMESPACE)
{
$this->session = $session;
$this->namespace = $namespace;
}
/**
* {@inheritdoc}
*/
public function getToken($tokenId, $default = null)
{
if (!$this->session->isStarted()) {
$this->session->start();
}
return $this->session->get($this->namespace . '/' . $tokenId, $default);
}
/**
* {@inheritdoc}
*/
public function setToken($tokenId, $token)
{
if (!$this->session->isStarted()) {
$this->session->start();
}
$this->session->set($this->namespace . '/' . $tokenId, $token);
}
/**
* {@inheritdoc}
*/
public function hasToken($tokenId)
{
if (!$this->session->isStarted()) {
$this->session->start();
}
return $this->session->has($this->namespace . '/' . $tokenId);
}
}

View File

@ -0,0 +1,49 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Security\Csrf\TokenStorage;
/**
* Stores CSRF tokens.
*
* @since 2.4
* @author Bernhard Schussek <bschussek@gmail.com>
*/
interface TokenStorageInterface
{
/**
* Reads a stored CSRF token.
*
* @param string $tokenId The token ID
* @param mixed $default The value to be returned if no token is set
*
* @return mixed The stored token or the default value, if no token is set
*/
public function getToken($tokenId, $default = null);
/**
* Stores a CSRF token.
*
* @param string $tokenId The token ID
* @param mixed $token The CSRF token
*/
public function setToken($tokenId, $token);
/**
* Checks whether a token with the given token ID exists.
*
* @param string $tokenId The token ID
*
* @return Boolean Returns true if a token is stored for the given token ID,
* false otherwise.
*/
public function hasToken($tokenId);
}

View File

@ -0,0 +1,34 @@
{
"name": "symfony/security-csrf",
"type": "library",
"description": "Symfony Security Component - CSRF Library",
"keywords": [],
"homepage": "http://symfony.com",
"license": "MIT",
"authors": [
{
"name": "Fabien Potencier",
"email": "fabien@symfony.com"
},
{
"name": "Symfony Community",
"homepage": "http://symfony.com/contributors"
}
],
"require": {
"php": ">=5.3.3"
},
"require-dev": {
"symfony/security-core": "~2.4"
},
"autoload": {
"psr-0": { "Symfony\\Component\\Security\\Csrf\\": "" }
},
"target-dir": "Symfony/Component/Security/Csrf",
"minimum-stability": "dev",
"extra": {
"branch-alias": {
"dev-master": "2.4-dev"
}
}
}

View File

@ -0,0 +1,29 @@
<?xml version="1.0" encoding="UTF-8"?>
<phpunit backupGlobals="false"
backupStaticAttributes="false"
colors="true"
convertErrorsToExceptions="true"
convertNoticesToExceptions="true"
convertWarningsToExceptions="true"
processIsolation="false"
stopOnFailure="false"
syntaxCheck="false"
bootstrap="vendor/autoload.php"
>
<testsuites>
<testsuite name="Symfony Security Component CSRF Test Suite">
<directory>./Tests/</directory>
</testsuite>
</testsuites>
<filter>
<whitelist>
<directory>./</directory>
<exclude>
<directory>./vendor</directory>
<directory>./Tests</directory>
</exclude>
</whitelist>
</filter>
</phpunit>

View File

@ -24,6 +24,7 @@
"replace": {
"symfony/security-acl": "self.version",
"symfony/security-core": "self.version",
"symfony/security-csrf": "self.version",
"symfony/security-http": "self.version"
},
"require-dev": {