Skip to content

Commit

Permalink
Merge branch 'master' into 65-csrf-header
Browse files Browse the repository at this point in the history
  • Loading branch information
samdark authored Nov 7, 2024
2 parents 1792f4d + 38ea3c5 commit f13635f
Show file tree
Hide file tree
Showing 10 changed files with 455 additions and 13 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

- New #68: Add the `CsrfHeaderMiddleware` middleware to use custom HTTP header to prevent forgery of requests (@olegbaturin)
- Enh #68: Add the `CsrfMiddleware::withSafeMethods()` method to configure a custom safe HTTP methods list (@olegbaturin)
- Chg #71: Deprecate `CsrfMiddleware` in favor of `CsrfTokenMiddleware` (@ev-gor)

## 2.1.1 May 08, 2024

Expand Down
8 changes: 4 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ composer require yiisoft/csrf

## General usage

In order to enable CSRF protection you need to add `CsrfMiddleware` to your main middleware stack.
In order to enable CSRF protection you need to add `CsrfTokenMiddleware` to your main middleware stack.
In Yii it is done by configuring `config/web/application.php`:

```php
Expand All @@ -48,7 +48,7 @@ return [
[
ErrorCatcher::class,
SessionMiddleware::class,
CsrfMiddleware::class, // <-- add this
CsrfTokenMiddleware::class, // <-- add this
Router::class,
]
);
Expand All @@ -74,7 +74,7 @@ You can change this behavior by implementing your own request handler:
use Psr\Http\Message\ResponseFactoryInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\RequestHandlerInterface;
use Yiisoft\Csrf\CsrfMiddleware;
use Yiisoft\Csrf\CsrfTokenMiddleware;

/**
* @var Psr\Http\Message\ResponseFactoryInterface $responseFactory
Expand All @@ -99,7 +99,7 @@ $failureHandler = new class ($responseFactory) implements RequestHandlerInterfac
}
};

$middleware = new CsrfMiddleware($responseFactory, $csrfToken, $failureHandler);
$middleware = new CsrfTokenMiddleware($responseFactory, $csrfToken, $failureHandler);
```

By default, `CsrfMiddleware` considers `GET`, `HEAD`, `OPTIONS` methods as safe operations and doesn't perform CSRF validation. You can change this behavior as follows:
Expand Down
1 change: 1 addition & 0 deletions src/CsrfMiddleware.php
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
* PSR-15 middleware that takes care of token validation.
*
* @link https://www.php-fig.org/psr/psr-15/
* @deprecated Use the {@see CsrfTokenMiddleware} class instead.
*/
final class CsrfMiddleware implements MiddlewareInterface
{
Expand Down
109 changes: 109 additions & 0 deletions src/CsrfTokenMiddleware.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
<?php

declare(strict_types=1);

namespace Yiisoft\Csrf;

use Psr\Http\Message\ResponseFactoryInterface;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\MiddlewareInterface;
use Psr\Http\Server\RequestHandlerInterface;
use Yiisoft\Http\Method;
use Yiisoft\Http\Status;

use function in_array;
use function is_string;

/**
* PSR-15 middleware that takes care of token validation.
*
* @link https://www.php-fig.org/psr/psr-15/
*/
final class CsrfTokenMiddleware implements MiddlewareInterface
{
public const PARAMETER_NAME = '_csrf';
public const HEADER_NAME = 'X-CSRF-Token';

private string $parameterName = self::PARAMETER_NAME;
private string $headerName = self::HEADER_NAME;

private ResponseFactoryInterface $responseFactory;
private CsrfTokenInterface $token;
private ?RequestHandlerInterface $failureHandler;

public function __construct(
ResponseFactoryInterface $responseFactory,
CsrfTokenInterface $token,
RequestHandlerInterface $failureHandler = null
) {
$this->responseFactory = $responseFactory;
$this->token = $token;
$this->failureHandler = $failureHandler;
}

public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
{
if ($this->validateCsrfToken($request)) {
return $handler->handle($request);
}

if ($this->failureHandler !== null) {
return $this->failureHandler->handle($request);
}

$response = $this->responseFactory->createResponse(Status::UNPROCESSABLE_ENTITY);
$response
->getBody()
->write(Status::TEXTS[Status::UNPROCESSABLE_ENTITY]);
return $response;
}

public function withParameterName(string $name): self
{
$new = clone $this;
$new->parameterName = $name;
return $new;
}

public function withHeaderName(string $name): self
{
$new = clone $this;
$new->headerName = $name;
return $new;
}

public function getParameterName(): string
{
return $this->parameterName;
}

public function getHeaderName(): string
{
return $this->headerName;
}

private function validateCsrfToken(ServerRequestInterface $request): bool
{
if (in_array($request->getMethod(), [Method::GET, Method::HEAD, Method::OPTIONS], true)) {
return true;
}

$token = $this->getTokenFromRequest($request);

return !empty($token) && $this->token->validate($token);
}

private function getTokenFromRequest(ServerRequestInterface $request): ?string
{
$parsedBody = $request->getParsedBody();

$token = $parsedBody[$this->parameterName] ?? null;
if (empty($token)) {
$headers = $request->getHeader($this->headerName);
$token = reset($headers);
}

return is_string($token) ? $token : null;
}
}
10 changes: 5 additions & 5 deletions tests/CsrfMiddlewareTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@

use Nyholm\Psr7\Factory\Psr17Factory;
use PHPUnit\Framework\TestCase;
use Yiisoft\Csrf\CsrfMiddleware;
use Yiisoft\Csrf\CsrfTokenMiddleware;
use Yiisoft\Csrf\Synchronizer\Generator\RandomCsrfTokenGenerator;
use Yiisoft\Csrf\Synchronizer\SynchronizerCsrfToken;
use Yiisoft\Csrf\Tests\Synchronizer\Storage\MockCsrfTokenStorage;
Expand All @@ -17,7 +17,7 @@ final class CsrfMiddlewareTest extends TestCase
public function testDefaultParameterName(): void
{
$middleware = $this->createMiddleware();
$this->assertSame(CsrfMiddleware::PARAMETER_NAME, $middleware->getParameterName());
$this->assertSame(CsrfTokenMiddleware::PARAMETER_NAME, $middleware->getParameterName());
}

public function testGetParameterName(): void
Expand All @@ -31,7 +31,7 @@ public function testGetParameterName(): void
public function testDefaultHeaderName(): void
{
$middleware = $this->createMiddleware();
$this->assertSame(CsrfMiddleware::HEADER_NAME, $middleware->getHeaderName());
$this->assertSame(CsrfTokenMiddleware::HEADER_NAME, $middleware->getHeaderName());
}

public function testGetHeaderName(): void
Expand All @@ -50,9 +50,9 @@ public function testImmutability(): void
$this->assertNotSame($original, $original->withSafeMethods([Method::HEAD]));
}

private function createMiddleware(): CsrfMiddleware
private function createMiddleware(): CsrfTokenMiddleware
{
return new CsrfMiddleware(
return new CsrfTokenMiddleware(
new Psr17Factory(),
new SynchronizerCsrfToken(
new RandomCsrfTokenGenerator(),
Expand Down
61 changes: 61 additions & 0 deletions tests/DeprecatedCsrfMiddlewareTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
<?php

declare(strict_types=1);

namespace Yiisoft\Csrf\Tests;

use Nyholm\Psr7\Factory\Psr17Factory;
use PHPUnit\Framework\TestCase;
use Yiisoft\Csrf\CsrfMiddleware;
use Yiisoft\Csrf\Synchronizer\Generator\RandomCsrfTokenGenerator;
use Yiisoft\Csrf\Synchronizer\SynchronizerCsrfToken;
use Yiisoft\Csrf\Tests\Synchronizer\Storage\MockCsrfTokenStorage;

final class DeprecatedCsrfMiddlewareTest extends TestCase
{
public function testDefaultParameterName(): void
{
$middleware = $this->createMiddleware();
$this->assertSame(CsrfMiddleware::PARAMETER_NAME, $middleware->getParameterName());
}

public function testGetParameterName(): void
{
$middleware = $this
->createMiddleware()
->withParameterName('my-csrf');
$this->assertSame('my-csrf', $middleware->getParameterName());
}

public function testDefaultHeaderName(): void
{
$middleware = $this->createMiddleware();
$this->assertSame(CsrfMiddleware::HEADER_NAME, $middleware->getHeaderName());
}

public function testGetHeaderName(): void
{
$middleware = $this
->createMiddleware()
->withHeaderName('MY-CSRF');
$this->assertSame('MY-CSRF', $middleware->getHeaderName());
}

public function testImmutability(): void
{
$original = $this->createMiddleware();
$this->assertNotSame($original, $original->withHeaderName('csrf'));
$this->assertNotSame($original, $original->withParameterName('csrf'));
}

private function createMiddleware(): CsrfMiddleware
{
return new CsrfMiddleware(
new Psr17Factory(),
new SynchronizerCsrfToken(
new RandomCsrfTokenGenerator(),
new MockCsrfTokenStorage()
)
);
}
}
Loading

0 comments on commit f13635f

Please sign in to comment.