溶剂t/csrf-protection

PSR-15 兼容的中间件,实现跨站请求伪造保护

1.0 2021-11-25 10:59 UTC

README

目录

  1. 特性
  2. 安装
  3. 使用
  4. 真实用例
  5. 自定义令牌名称
  6. 自定义失败处理器
  7. 自定义令牌存储
  8. 自定义令牌生成算法
  9. 自定义CSRF令牌类
  10. 在自定义请求头中包含CSRF令牌

这是一个PSR-15兼容的中间件,实现了对跨站请求伪造的保护。

在这个包中,CSRF保护是根据OWASP网站(OWASP)上描述的Synchronizer Token模式组织的。

特性

此包使用令牌掩码(通过与随机密钥进行XOR操作进行随机化)。此方法建议用于防御BREACH攻击。

CSRF令牌在每个会话中只生成和保存一次(这可以更改)。但是,由于掩码,每次请求时令牌都是唯一的

掩码令牌消除了在浏览器中点击“后退”按钮时,在服务器上发生错误的CSRF触发问题。

安装

// php 7.4+
composer require solventt/csrf-protection ^0.1

// php 8.0+
composer require solventt/csrf-protection ^1.0

使用

$csrfToken = new MaskedCsrfToken(new SessionTokenStorage(), new SecurityHelper());

$middleware = new CsrfMiddleware($csrfToken, new ResponseFactory());

// then add the middleware to the middlewares stack

要获取令牌的名称和有效值,请执行以下操作:

// data for a hidden HTML form field

$name = $csrfToken->getName();

$value = $csrfToken->getValue();

HTML中的某个地方

<input type="hidden" name="<?= $name ?>" value="<?= $value ?>">

当第一次调用getValue()方法时,CSRF令牌生成并存储到存储中(通常在用户会话中)。在随后的方法调用中,从存储中获取CSRF令牌值。

默认情况下,getValue()方法返回一个掩码令牌。如果您需要存储在会话中的CSRF令牌的原始值,请在getValue()方法中将第一个参数指定为false

$value = $csrfToken->getValue(false);

如果您想生成特定长度的令牌,请将长度指定为getValue()方法的第二个参数。

$value = $csrfToken->getValue(true, 30);

默认令牌长度为32个字符,不能小于15。

由于CSRF令牌是随机掩码的,因此不需要在同一个会话中重新生成它。但如果需要,请执行以下操作:

// a default length of the token is 32 chars
$csrfToken->regenerate();

// you can specify a different length
$csrfToken->regenerate(35);

真实用例

这是一个在Slim微框架中使用CSRF保护示例。

config/csrf.php

// the DI container definition

// a constructor of the CsrfMiddleware class has 2 mandatory arguments: $token and $responseFactory.
// Thanks to the dependency injection container, the CsrfTokenInterface and ResponseFactoryInterface
// dependencies will be automatically resolved during CsrfMiddleware instantiation

return [
    CsrfTokenInterface::class => function (ContainerInterface $c) {
        return new MaskedCsrfToken(new SessionTokenStorage(), new SecurityHelper());
    },

    ResponseFactoryInterface::class => fn () => new ResponseFactory(),
];

routing/middleware.php

/**
* Adding the middleware to the stack
*
* @var Slim\App $app
*/
$app->add(CsrfMiddleware::class);

config/twig.php

// the DI container definition

Environment::class => function (ContainerInterface $c) {
    
    ...
    
    $csrf = new TwigFunction('csrf', function () use ($c): string {

        /** @var MaskedCsrfToken $csrf */
        $csrf = $c->get(CsrfTokenInterface::class);

        $name = $csrf->getName();
        $token = $csrf->getValue();

        return sprintf('<input type="hidden" name="%s" value="%s">', $name, $token);
    });
    
    $twig->addFunction($csrf);
    
    ...
}

views/template.twig

...

{{ csrf()|raw }}

...

自定义令牌名称

默认令牌名称是_csrf。但您可以通过将其添加为MaskedCsrfToken构造函数的第三个参数来自定义名称。

$csrfToken = new MaskedCsrfToken(
    new SessionTokenStorage(),
    new SecurityHelper(),
    'customTokenName'
);

自定义失败处理器

默认情况下,如果CSRF令牌不匹配,客户端将收到代码400和“Bad Request”消息。

但您可以定义自己的逻辑来处理CSRF失败。只需将匿名函数作为CsrfMiddleware构造函数的第三个参数添加即可。

...

$session = $container->get(SessionInterface::class);

$logger = $container->get(LoggerInterface::class);

$responseFactory = $container->get(ResponseFactoryInterface::class);

$failureHandler = function () use ($session, $logger, $responseFactory): ResponseInterface {
    $session->destroy();
    $logger->error('CSRF check failed');
    $response = $responseFactory->createResponse(403);
    $response->getBody()->write('Forbidden');
    
    return $response;
};

$middleware = new CsrfMiddleware(
    $csrfToken, 
    new ResponseFactory(),
    $failureHandler
);

注意:匿名函数必须返回一个实现ResponseInterface的实例。

自定义令牌存储

默认情况下,此包提供了直接与超全局$_SESSION一起工作的SessionTokenStorage类。如果不是这样,您可以编写自己的令牌存储版本。然后,您的类必须实现TokenStorageInterface接口。

interface TokenStorageInterface
{
    public function get(string $tokenName): ?string;
    public function set(string $tokenName, string $value): void;
    public function remove(string $tokenName): void;
}

例如,如果您的代码使用对$_SESSION的抽象来处理会话,则您的令牌存储可能如下所示:

use Solventt\Csrf\Interfaces\TokenStorageInterface;
use Odan\Session\SessionInterface;

class CsrfSessionTokenStorage implements TokenStorageInterface
{
    public function __construct(private SessionInterface $session) {}

    public function get(string $tokenName): ?string
    {
        /** @var mixed|null $value */
        $value = $this->session->get($tokenName);

        return is_string($value) ? $value : null;
    }

    public function set(string $tokenName, string $value): void
    {
        $this->session->set($tokenName, $value);
    }

    public function remove(string $tokenName): void
    {
        $this->session->remove($tokenName);
    }

自定义令牌生成算法

您可以为生成CSRF令牌和添加/删除令牌掩码定义自己的逻辑。为此,您的类必须实现SecurityInterface接口。

interface SecurityInterface
{
    /** 
    * Generates a cryptographically secure value  
    */
    public function generateToken(int $length): string;
    
    /** 
    * Applies a random mask to the CSRF token making it unique when its requested 
    */
    public function addMask(string $token): string;
    
    /** 
    * Removes the mask from the CSRF token previously masked with the 'addMask' method 
    */
    public function removeMask(string $token): string;
}

自定义CSRF令牌类

此包提供了代表CSRF令牌的MaskedCsrfToken类。但您可以根据CsrfTokenInterface编写自己的令牌实现。

interface CsrfTokenInterface
{
    public const DEFAULT_NAME = '_csrf';

    public function getName(): string;
    public function getValue(): string;
    
    /** 
    * Compares the token from the request with the token found in a token storage
    */
    public function equals(string $requestToken): bool;
}

在自定义请求头中包含CSRF令牌

如果请求体中没有找到CSRF令牌,中间件将检查X-CSRF-Token头。您可以使用setHeaderName方法提供自己的头名称。

/**  
 * @var CsrfMiddleware $middleware 
 */
$middleware->setHeaderName('X-CUSTOM-HEADER');

例如,对于AJAX请求,这是相关的。