溶剂t / csrf-protection
PSR-15 兼容的中间件,实现跨站请求伪造保护
Requires
- php: ^7.4 || ^8.0
- psr/http-factory: ^1.0
- psr/http-server-middleware: ^1.0
Requires (Dev)
- phpunit/phpunit: ^9.5
- slim/psr7: ^1.5
- squizlabs/php_codesniffer: ^3.6
- vimeo/psalm: ^4.10
This package is auto-updated.
Last update: 2024-08-31 00:28:02 UTC
README
目录
这是一个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请求,这是相关的。