generated from yiisoft/package-template
-
-
Notifications
You must be signed in to change notification settings - Fork 10
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
added CsrfHeaderMiddleware #68
Open
olegbaturin
wants to merge
13
commits into
yiisoft:master
Choose a base branch
from
olegbaturin:65-csrf-header
base: master
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
13 commits
Select commit
Hold shift + click to select a range
78afec8
added CsrfHeaderMiddlaware
olegbaturin 6de25c9
fix style
olegbaturin ca96d0c
update CsrfHeaderMiddleware unsafe methods
olegbaturin 770397a
update readme
olegbaturin 9f9b6ce
update readme
olegbaturin fe7ef3e
fix eradme
olegbaturin 4ce120a
update changelog
olegbaturin 188ce88
update readme
olegbaturin a12399d
remove redundant methods
olegbaturin 927ebbe
fix readme
olegbaturin dfe9452
update readme
olegbaturin c8577a5
update readme
olegbaturin 1792f4d
Merge branch 'master' into 65-csrf-header
vjik File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change | ||||
---|---|---|---|---|---|---|
|
@@ -102,6 +102,37 @@ $failureHandler = new class ($responseFactory) implements RequestHandlerInterfac | |||||
$middleware = new CsrfMiddleware($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: | ||||||
|
||||||
```php | ||||||
use Yiisoft\Csrf\CsrfMiddleware; | ||||||
use Yiisoft\Http\Method; | ||||||
|
||||||
$csrfMiddleware = $container->get(CsrfMiddleware::class); | ||||||
|
||||||
// Returns a new instance with the specified list of safe methods. | ||||||
$csrfMiddleware = $csrfMiddleware->withSafeMethods([Method::OPTIONS]); | ||||||
|
||||||
// Returns a new instance with the specified header name. | ||||||
$csrfMiddleware = $csrfMiddleware->withHeaderName('X-CSRF-PROTECTION'); | ||||||
``` | ||||||
|
||||||
or define the `CsrfMiddleware` configuration in the DI container: | ||||||
|
||||||
`config/web/di/csrf.php` | ||||||
|
||||||
```php | ||||||
use Yiisoft\Csrf\CsrfMiddleware; | ||||||
use Yiisoft\Http\Method; | ||||||
|
||||||
return [ | ||||||
CsrfMiddleware::class => [ | ||||||
'withSafeMethods()' => [[Method::OPTIONS]], | ||||||
'withHeaderName()' => ['X-CSRF-PROTECTION'], | ||||||
], | ||||||
]; | ||||||
``` | ||||||
|
||||||
## CSRF Tokens | ||||||
|
||||||
In case Yii framework is used along with config plugin, the package is [configured](./config/web.php) | ||||||
|
@@ -157,6 +188,300 @@ the next request either as a hidden form field or via JavaScript async request. | |||||
|
||||||
It is recommended to always use this decorator. | ||||||
|
||||||
## CSRF protection for AJAX/SPA backend API | ||||||
|
||||||
If you are using a cookie to authenticate your AJAX/SPA, then you do need CSRF protection for the backend API. | ||||||
|
||||||
### Employing custom request header | ||||||
|
||||||
In this pattern, AJAX/SPA frontend appends a custom header to API requests that require CSRF protection. No token is needed for this approach. This defense relies on the CORS preflight mechanism which sends an `OPTIONS` request to verify CORS compliance with the destination server. All modern browsers, according to the same-origin policy security model, designate requests with custom headers as "to be preflighted". When the API requires a custom header, you know that the request must have been preflighted if it came from a browser. | ||||||
|
||||||
The header can be any arbitrary key-value pair, as long as it does not conflict with existing headers. Empty value is also acceptable. | ||||||
|
||||||
``` | ||||||
X-CSRF-HEADER=1 | ||||||
``` | ||||||
|
||||||
When handling the request, the API checks for the existence of this header. If the header does not exist, the backend rejects the request as potential forgery. Employing a custom header allows to reject [simple requests](https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS#simple_requests) that browsers do not designate as "to be preflighted" and permit them to be sent to any origin. | ||||||
|
||||||
In order to enable CSRF protection you need to add `CsrfHeaderMiddleware` to your `RouteCollectorInterface` configuration | ||||||
|
||||||
```php | ||||||
return [ | ||||||
Yiisoft\Yii\Http\Application::class => [ | ||||||
'__construct()' => [ | ||||||
'dispatcher' => DynamicReference::to(static function (Injector $injector) { | ||||||
return ($injector->make(MiddlewareDispatcher::class)) | ||||||
->withMiddlewares( | ||||||
[ | ||||||
ErrorCatcher::class, | ||||||
CsrfHeaderMiddleware::class, // <-- add this | ||||||
Router::class, | ||||||
] | ||||||
); | ||||||
}), | ||||||
], | ||||||
], | ||||||
]; | ||||||
``` | ||||||
|
||||||
or to the routes that must be protected. | ||||||
|
||||||
```php | ||||||
return [ | ||||||
RouteCollectionInterface::class => static function (RouteCollectorInterface $collector) use ($config) { | ||||||
$collector | ||||||
->middleware(CsrfHeaderMiddleware::class) // <-- add this | ||||||
->addGroup(Group::create(null)->routes($routes)); | ||||||
|
||||||
return new RouteCollection($collector); | ||||||
}, | ||||||
]; | ||||||
``` | ||||||
|
||||||
By default, `CsrfHeaderMiddleware` considers only `GET`, `HEAD`, `POST` methods as unsafe operations. Requests with other HTTP methods trigger CORS preflight and do not require CSRF header validation. You can change this behavior as follows: | ||||||
|
||||||
```php | ||||||
use Yiisoft\Csrf\CsrfHeaderMiddleware; | ||||||
use Yiisoft\Http\Method; | ||||||
|
||||||
$csrfHeaderMiddleware = $container->get(CsrfHeaderMiddleware::class); | ||||||
|
||||||
// Returns a new instance with the specified list of unsafe methods. | ||||||
$csrfHeaderMiddleware = $csrfHeaderMiddleware->withUnsafeMethods([Method::POST]); | ||||||
|
||||||
// Returns a new instance with the specified header name. | ||||||
$csrfHeaderMiddleware = $csrfHeaderMiddleware->withHeaderName('X-CSRF-PROTECTION'); | ||||||
``` | ||||||
|
||||||
or define the `CsrfHeaderMiddleware` configuration in the DI container: | ||||||
|
||||||
`config/web/di/csrf.php` | ||||||
Comment on lines
+258
to
+259
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||
|
||||||
```php | ||||||
use Yiisoft\Csrf\CsrfHeaderMiddleware; | ||||||
use Yiisoft\Http\Method; | ||||||
|
||||||
return [ | ||||||
CsrfHeaderMiddleware::class => [ | ||||||
'withUnsafeMethods()' => [[Method::POST]], | ||||||
'withHeaderName()' => ['X-CSRF-PROTECTION'], | ||||||
], | ||||||
]; | ||||||
``` | ||||||
|
||||||
The use of a custom request header for CSRF protection is based on the CORS Protocol. Thus, you **must** configure the CORS module to allow or deny cross-origin access to the backend API. | ||||||
|
||||||
> [!IMPORTANT] | ||||||
> `CsrfHeaderMiddleware` can be used to prevent forgery of same-origin requests and requests from the list of specific origins only. | ||||||
|
||||||
|
||||||
### Protecting same-origin requests | ||||||
|
||||||
In this scenario | ||||||
- AJAX/SPA frontend and API backend have the same origin. | ||||||
- Cross-origin requests to the API server are denied. | ||||||
- Simple CORS requests must be restricted. | ||||||
|
||||||
#### Configure CORS module | ||||||
|
||||||
- Responses to a CORS preflight requests **must not** contain CORS headers. | ||||||
- Responses to an actual requests **must not** contain CORS headers. | ||||||
|
||||||
#### Configure middlewares stack | ||||||
|
||||||
Add `CsrfHeaderMiddleware` to the main middleware stack: | ||||||
|
||||||
```php | ||||||
$middlewareDispatcher = $injector->make(MiddlewareDispatcher::class); | ||||||
$middlewareDispatcher = $middlewareDispatcher->withMiddlewares([ | ||||||
ErrorCatcher::class, | ||||||
CsrfHeaderMiddleware::class, // <-- add this | ||||||
Router::class, | ||||||
]); | ||||||
``` | ||||||
|
||||||
or to the routes that must be protected: | ||||||
|
||||||
```php | ||||||
$collector = $container->get(RouteCollectorInterface::class); | ||||||
$collector->addGroup( | ||||||
Group::create('/api') | ||||||
->middleware(CsrfHeaderMiddleware::class) // <-- add this | ||||||
->routes($routes) | ||||||
); | ||||||
``` | ||||||
|
||||||
#### Configure frontend requests | ||||||
|
||||||
On the frontend add to the `GET`, `HEAD`, `POST` requests a custom header defined in the `CsrfHeaderMiddleware` with an empty or random value. | ||||||
|
||||||
```js | ||||||
let response = fetch('https://example.com/api/whoami', { | ||||||
headers: { | ||||||
"X-CSRF-HEADER": crypto.randomUUID() | ||||||
} | ||||||
}); | ||||||
``` | ||||||
|
||||||
### Protecting requests from the list of specific origins | ||||||
|
||||||
In this scenario: | ||||||
- AJAX/SPA frontend and API backend have different origins. | ||||||
- Allow cross origin requests to the API server from the list of specific origins only. | ||||||
- Simple CORS requests must be restricted. | ||||||
|
||||||
#### Configure CORS module | ||||||
|
||||||
- A successful responses to a CORS preflight requests **must** contain appropriate CORS headers. | ||||||
- Responses to an actual requests **must** contain appropriate CORS headers. | ||||||
- Value of the CORS header `Access-Control-Allow-Origin` **must** contains origin from the predefined list. | ||||||
|
||||||
``` | ||||||
// assuming frontend origin is https://example.com and backend origin is https://api.example.com | ||||||
Access-Control-Allow-Origin: https://example.com | ||||||
``` | ||||||
|
||||||
#### Configure middlewares stack | ||||||
|
||||||
Add `CsrfHeaderMiddleware` to the main middleware stack: | ||||||
|
||||||
```php | ||||||
$middlewareDispatcher = $injector->make(MiddlewareDispatcher::class); | ||||||
$middlewareDispatcher = $middlewareDispatcher->withMiddlewares([ | ||||||
ErrorCatcher::class, | ||||||
CsrfHeaderMiddleware::class, // <-- add this | ||||||
Router::class, | ||||||
]); | ||||||
``` | ||||||
|
||||||
or to the routes that must be protected: | ||||||
|
||||||
```php | ||||||
$collector = $container->get(RouteCollectorInterface::class); | ||||||
$collector->addGroup( | ||||||
Group::create('/api') | ||||||
->middleware(CsrfHeaderMiddleware::class) // <-- add this | ||||||
->routes($routes) | ||||||
); | ||||||
``` | ||||||
|
||||||
#### Configure frontend requests | ||||||
|
||||||
On the frontend add to the `GET`, `HEAD`, `POST` requests a custom header defined in the `CsrfHeaderMiddleware` with an empty or random value. | ||||||
|
||||||
```js | ||||||
let response = fetch('https://api.example.com/whoami', { | ||||||
headers: { | ||||||
"X-CSRF-HEADER": crypto.randomUUID() | ||||||
} | ||||||
}); | ||||||
``` | ||||||
|
||||||
### Protecting requests passed from any origin | ||||||
|
||||||
In this scenario: | ||||||
- AJAX/SPA frontend and API backend have different origins. | ||||||
- Allow cross origin requests to the API server from any origin. | ||||||
- All requests are considered unsafe and **must** be protected against CSRF with CSRF-token. | ||||||
|
||||||
#### Configure CORS module | ||||||
|
||||||
- A successful responses to a CORS preflight requests **must** contain appropriate CORS headers. | ||||||
- Responses to an actual requests **must** contain appropriate CORS headers. | ||||||
- The CORS header `Access-Control-Allow-Origin` has the same value as `Origin` header in the request. | ||||||
|
||||||
``` | ||||||
$frontendOrigin = $request->getOrigin(); | ||||||
|
||||||
Access-Control-Allow-Origin: $frontendOrigin | ||||||
``` | ||||||
|
||||||
#### Configure middlewares stack | ||||||
|
||||||
By default, `CsrfMiddleware` considers `GET`, `HEAD`, `OPTIONS` methods as safe operations and doesn't perform CSRF validation. | ||||||
In JavaScript-based apps, requests are made programmatically; therefore, to increase application protection, the only `OPTIONS` method can be considered safe and need not be appended with a CSRF token header. | ||||||
|
||||||
Configure `CsrfMiddleware` safe methods: | ||||||
|
||||||
```php | ||||||
$csrfMiddleware = $container->get(CsrfMiddleware::class); | ||||||
$csrfMiddleware = $csrfMiddleware->withSafeMethods([Method::OPTIONS]); | ||||||
``` | ||||||
|
||||||
Add `CsrfMiddleware` to the main middleware stack: | ||||||
|
||||||
```php | ||||||
$middlewareDispatcher = $injector->make(MiddlewareDispatcher::class); | ||||||
$middlewareDispatcher = $middlewareDispatcher->withMiddlewares([ | ||||||
ErrorCatcher::class, | ||||||
SessionMiddleware::class, | ||||||
CsrfMiddleware::class, // <-- add this | ||||||
Router::class, | ||||||
]); | ||||||
``` | ||||||
|
||||||
or to the routes that must be protected: | ||||||
|
||||||
```php | ||||||
$collector = $container->get(RouteCollectorInterface::class); | ||||||
$collector->addGroup( | ||||||
Group::create('/api') | ||||||
->middleware(CsrfMiddleware::class) // <-- add this | ||||||
->routes($routes) | ||||||
); | ||||||
``` | ||||||
|
||||||
#### Configure routes | ||||||
|
||||||
Create a route for acquiring CSRF-tokens from the frontend application. | ||||||
|
||||||
```php | ||||||
use Psr\Http\Message\ResponseFactoryInterface; | ||||||
use Psr\Http\Message\ResponseInterface; | ||||||
use Yiisoft\Http\Header; | ||||||
use Yiisoft\Http\Method; | ||||||
use Yiisoft\Router\Route; | ||||||
|
||||||
Route::options('/csrf-token') | ||||||
->action(static function ( | ||||||
ResponseFactoryInterface $responseFactory, | ||||||
CsrfTokenInterface $token | ||||||
): ResponseInterface { | ||||||
$tokenValue = $token->getValue(); | ||||||
|
||||||
$response = $responseFactory->createResponse() | ||||||
->withHeader(Header::ALLOW, Method::OPTIONS) | ||||||
->withHeader('X-CSRF-TOKEN', $tokenValue); | ||||||
|
||||||
$response->getBody()->write($tokenValue); | ||||||
|
||||||
return $response; | ||||||
}), | ||||||
``` | ||||||
|
||||||
#### Configure frontend requests | ||||||
|
||||||
On the frontend first make a request to the configured endpoint and acquire a CSRF-token to use it in the subsequent requests. | ||||||
|
||||||
```js | ||||||
let response = await fetch('https://api.example.com/csrf-token'); | ||||||
|
||||||
let csrfToken = await response.text(); | ||||||
// OR | ||||||
let csrfToken = response.headers.get('X-CSRF-TOKEN'); | ||||||
``` | ||||||
|
||||||
Add to all requests a custom header defined in the `CsrfMiddleware` with acquired CSRF-token value. | ||||||
|
||||||
```js | ||||||
let response = fetch('https://api.example.com/whoami', { | ||||||
headers: { | ||||||
"X-CSRF-TOKEN": csrfToken | ||||||
} | ||||||
}); | ||||||
``` | ||||||
|
||||||
## Documentation | ||||||
|
||||||
- [Internals](docs/internals.md) | ||||||
|
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.