├── src └── PSR7Csrf │ ├── Exception │ ├── ExceptionInterface.php │ ├── InvalidRequestParameterNameException.php │ ├── InvalidExpirationTimeException.php │ └── SessionAttributeNotFoundException.php │ ├── HttpMethod │ ├── IsSafeHttpRequestInterface.php │ └── IsSafeHttpRequest.php │ ├── RequestParameter │ ├── ExtractCSRFParameterInterface.php │ └── ExtractCSRFParameter.php │ ├── Session │ ├── ExtractUniqueKeyFromSessionInterface.php │ └── ExtractUniqueKeyFromSession.php │ ├── TokenGeneratorInterface.php │ ├── Factory.php │ ├── TokenGenerator.php │ └── CSRFCheckerMiddleware.php ├── humbug.json.dist ├── phpcs.xml.dist ├── phpunit.xml.dist ├── LICENSE ├── CONTRIBUTING.md ├── composer.json ├── CHANGELOG.md └── README.md /src/PSR7Csrf/Exception/ExceptionInterface.php: -------------------------------------------------------------------------------- 1 | 2 | 3 | code-reviews.io code-style 4 | ./src 5 | ./test 6 | ./test/PSR7CsrfTest/RequestParameter/ExtractCSRFParameterTest.php 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /src/PSR7Csrf/Exception/InvalidRequestParameterNameException.php: -------------------------------------------------------------------------------- 1 | 0 integer', $expirationTime)); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/PSR7Csrf/Exception/SessionAttributeNotFoundException.php: -------------------------------------------------------------------------------- 1 | getAttributes())) 18 | )); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/PSR7Csrf/HttpMethod/IsSafeHttpRequest.php: -------------------------------------------------------------------------------- 1 | safeMethods = array_map('strtoupper', $safeMethods); 22 | } 23 | 24 | public static function fromDefaultSafeMethods() : self 25 | { 26 | return new self('GET', 'HEAD', 'OPTIONS'); 27 | } 28 | 29 | public function __invoke(RequestInterface $request) : bool 30 | { 31 | return in_array(strtoupper($request->getMethod()), $this->safeMethods, self::STRICT_CHECKING); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/PSR7Csrf/Session/ExtractUniqueKeyFromSession.php: -------------------------------------------------------------------------------- 1 | uniqueIdKey = $uniqueIdKey; 21 | } 22 | 23 | public function __invoke(SessionInterface $session) : string 24 | { 25 | $uniqueKey = $session->get($this->uniqueIdKey, ''); 26 | 27 | if ('' === $uniqueKey || ! is_string($uniqueKey)) { 28 | $generatedKey = bin2hex(random_bytes(self::ENTROPY)); 29 | 30 | $session->set($this->uniqueIdKey, $generatedKey); 31 | 32 | return $generatedKey; 33 | } 34 | 35 | return $uniqueKey; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /phpunit.xml.dist: -------------------------------------------------------------------------------- 1 | 2 | 20 | 21 | ./test/PSR7CsrfTest 22 | 23 | 24 | 25 | ./src 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2015 Marco Pivetta 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of 4 | this software and associated documentation files (the "Software"), to deal in 5 | the Software without restriction, including without limitation the rights to 6 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies 7 | of the Software, and to permit persons to whom the Software is furnished to do 8 | so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | SOFTWARE. 20 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | * Coding standard for the project is [PSR-2](https://github.com/php-fig/fig-standards/blob/master/accepted/PSR-2-coding-style-guide.md) 4 | * The project will follow [object calisthenics](http://www.slideshare.net/guilhermeblanco/object-calisthenics-applied-to-php) 5 | * Any contribution must provide tests for additional/corrected scenarios 6 | * Any un-confirmed issue needs a failing test case before being accepted 7 | * Pull requests must be sent from a new hotfix/feature branch, not from `master`. 8 | 9 | ## Installation 10 | 11 | To install the project and run the tests, you need to clone it first: 12 | 13 | ```sh 14 | $ git clone git://github.com/Ocramius/PSR7Csrf.git 15 | ``` 16 | 17 | You will then need to run a composer installation: 18 | 19 | ```sh 20 | $ cd PSR7Csrf 21 | $ curl -s https://getcomposer.org/installer | php 22 | $ php composer.phar update 23 | ``` 24 | 25 | ## Testing 26 | 27 | The PHPUnit version to be used is the one installed as a dev- dependency via composer: 28 | 29 | ```sh 30 | $ ./vendor/bin/phpunit 31 | ``` 32 | 33 | Accepted coverage for new contributions is 80%. Any contribution not satisfying this requirement 34 | won't be merged. 35 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ocramius/psr7-csrf", 3 | "license": "MIT", 4 | "authors": [ 5 | { 6 | "name": "Marco Pivetta", 7 | "email": "ocramius@gmail.com", 8 | "homepage": "http://ocramius.github.io/", 9 | "role": "Developer" 10 | } 11 | ], 12 | "require": { 13 | "php": "^7.1.0", 14 | "psr/http-message": "^1.0.1", 15 | "lcobucci/jwt": "^3.2.2", 16 | "psr/http-server-handler": "^1.0.0", 17 | "psr/http-server-middleware": "^1.0.0", 18 | "psr7-sessions/storageless": "^4.0.0" 19 | }, 20 | "require-dev": { 21 | "phpunit/phpunit": "^6.5.5", 22 | "humbug/humbug": "^1.0.0-rc.0", 23 | "squizlabs/php_codesniffer": "^2.6.0" 24 | }, 25 | "autoload": { 26 | "psr-4": { 27 | "PSR7Csrf\\": "src/PSR7Csrf" 28 | } 29 | }, 30 | "autoload-dev": { 31 | "psr-4": { 32 | "PSR7CsrfTest\\": "test/PSR7CsrfTest" 33 | } 34 | }, 35 | "extra": { 36 | "branch-alias": { 37 | "dev-master": "3.0.x-dev" 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | This is a list of changes/improvements that were introduced in PSR7Csrf 2 | 3 | ## 2.0.0 4 | 5 | - This release aligns the `PSR7Csrf\CSRFCheckerMiddleware` implementation to 6 | the [PSR-15 `php-fig/http-server-middleware`](https://github.com/php-fig/http-server-middleware/tree/1.0.0) 7 | specification. 8 | 9 | This means that the signature of `PSR7Csrf\CSRFCheckerMiddleware` 10 | changed, and therefore you need to look for usages of this class and verify 11 | if the new signature is compatible with your API 12 | 13 | Specifically, `PSR7Csrf\CSRFCheckerMiddleware#__invoke()` was removed. 14 | 15 | - The minimum supported PHP version has been raised to `7.1.0` 16 | 17 | - the `PSR7Csrf\Factory::createDefaultCSRFCheckerMiddleware()` method now has 18 | a mandatory argument, which is the response to be produced in case of failed 19 | CSRF validation. This argument is mandatory, since PSR7Csrf won't couple you 20 | to a specific PSR-7 implementation. 21 | 22 | ## 1.0.2 23 | 24 | ### Fixed 25 | 26 | - Allow installation of [PSR7Session](https://github.com/Ocramius/PSR7Session) 27 | [2.0.0](https://github.com/Ocramius/PSR7Session/releases/tag/2.0.0) [#2](https://github.com/Ocramius/PSR7Csrf/pull/1) 28 | 29 | ## 1.0.1 30 | 31 | ### Fixed 32 | 33 | - Minor wording issues in [`README.md`](README.md] [#1](https://github.com/Ocramius/PSR7Csrf/pull/1) 34 | -------------------------------------------------------------------------------- /src/PSR7Csrf/RequestParameter/ExtractCSRFParameter.php: -------------------------------------------------------------------------------- 1 | csrfDataKey = $csrfDataKey; 24 | } 25 | 26 | public function __invoke(ServerRequestInterface $request) : string 27 | { 28 | /* @var $requestBody array */ 29 | $requestBody = $request->getParsedBody(); 30 | 31 | if (is_object($requestBody) && array_key_exists($this->csrfDataKey, (array) $requestBody)) { 32 | $arrayBody = (array) $requestBody; 33 | 34 | return $this->ensureThatTheValueIsAString($arrayBody[$this->csrfDataKey]); 35 | } 36 | 37 | if (is_array($requestBody) && array_key_exists($this->csrfDataKey, $requestBody)) { 38 | return $this->ensureThatTheValueIsAString($requestBody[$this->csrfDataKey]); 39 | } 40 | 41 | return ''; 42 | } 43 | 44 | private function ensureThatTheValueIsAString($value) : string 45 | { 46 | if (! is_string($value)) { 47 | return ''; 48 | } 49 | 50 | return $value; 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/PSR7Csrf/Factory.php: -------------------------------------------------------------------------------- 1 | signer = $signer; 57 | $this->extractUniqueKeyFromSession = $extractUniqueKeyFromSession; 58 | $this->expirationTime = $expirationTime; 59 | $this->sessionAttribute = $sessionAttribute; 60 | } 61 | 62 | public function __invoke(ServerRequestInterface $request) : Token 63 | { 64 | $session = $request->getAttribute($this->sessionAttribute); 65 | 66 | if (! $session instanceof SessionInterface) { 67 | throw SessionAttributeNotFoundException::fromAttributeNameAndRequest($this->sessionAttribute, $request); 68 | } 69 | 70 | $timestamp = (new \DateTime())->getTimestamp(); 71 | 72 | return (new Builder()) 73 | ->setIssuedAt($timestamp) 74 | ->setExpiration($timestamp + $this->expirationTime) 75 | ->sign($this->signer, $this->extractUniqueKeyFromSession->__invoke($session)) 76 | ->getToken(); 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /src/PSR7Csrf/CSRFCheckerMiddleware.php: -------------------------------------------------------------------------------- 1 | isSafeHttpRequest = $isSafeHttpRequest; 68 | $this->extractUniqueKeyFromSession = $extractUniqueKeyFromSession; 69 | $this->extractCSRFParameter = $extractCSRFParameter; 70 | $this->tokenParser = $tokenParser; 71 | $this->signer = $signer; 72 | $this->sessionAttribute = $sessionAttribute; 73 | $this->faultyResponse = $faultyResponse; 74 | } 75 | 76 | public function process( 77 | ServerRequestInterface $request, 78 | RequestHandlerInterface $handler 79 | ) : ResponseInterface { 80 | if ($this->isSafeHttpRequest->__invoke($request)) { 81 | return $handler->handle($request); 82 | } 83 | 84 | try { 85 | $token = $this->tokenParser->parse($this->extractCSRFParameter->__invoke($request)); 86 | 87 | if ($token->validate(new ValidationData()) 88 | && $token->verify( 89 | $this->signer, 90 | $this->extractUniqueKeyFromSession->__invoke($this->getSession($request)) 91 | ) 92 | ) { 93 | return $handler->handle($request); 94 | } 95 | } catch (BadMethodCallException $invalidToken) { 96 | return $this->faultyResponse; 97 | } catch (InvalidArgumentException $invalidToken) { 98 | return $this->faultyResponse; 99 | } 100 | 101 | return $this->faultyResponse; 102 | } 103 | 104 | private function getSession(ServerRequestInterface $request) : SessionInterface 105 | { 106 | $session = $request->getAttribute($this->sessionAttribute); 107 | 108 | if (! $session instanceof SessionInterface) { 109 | throw SessionAttributeNotFoundException::fromAttributeNameAndRequest($this->sessionAttribute, $request); 110 | } 111 | 112 | return $session; 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # PSR-7 Storage-less HTTP CSRF protection 2 | 3 | [![Build Status](https://travis-ci.org/Ocramius/PSR7Csrf.svg)](https://travis-ci.org/Ocramius/PSR7Csrf) 4 | [![Scrutinizer Code Quality](https://scrutinizer-ci.com/g/Ocramius/PSR7Csrf/badges/quality-score.png?b=master)](https://scrutinizer-ci.com/g/Ocramius/PSR7Csrf/?branch=master) 5 | [![Code Coverage](https://scrutinizer-ci.com/g/Ocramius/PSR7Csrf/badges/coverage.png?b=master)](https://scrutinizer-ci.com/g/Ocramius/PSR7Csrf/?branch=master) 6 | [![Packagist](https://img.shields.io/packagist/v/ocramius/psr7-csrf.svg)](https://packagist.org/packages/ocramius/psr7-csrf) 7 | [![Packagist](https://img.shields.io/packagist/vpre/ocramius/psr7-csrf.svg)](https://packagist.org/packages/ocramius/psr7-csrf) 8 | 9 | **PSR7Csrf** is a [PSR-7](http://www.php-fig.org/psr/psr-7/) 10 | [middleware](https://mwop.net/blog/2015-01-08-on-http-middleware-and-psr-7.html) that enables 11 | [CSRF](https://www.owasp.org/index.php/Cross-Site_Request_Forgery_(CSRF)) protection for PSR-7 based applications. 12 | 13 | # DEPRECATED in favor of `psr7-sessions/storageless` 5.0.0+ 14 | 15 | Please note that this package is **DEPRECATED**. 16 | 17 | Since [`psr7-sessions/storageless` 5.0.0](https://github.com/psr7-sessions/storageless/releases/tag/5.0.0), 18 | the generated cookies are CSRF-resistant by default for unsafe HTTP methods (`POST`/`PUT`/`DELETE`/`PATCH`/etc.), 19 | so the usage of this package is no longer needed. 20 | You can still install `ocramius/psr7-csrf`, but since there is no practical need for it, 21 | it is not necessary to do so. 22 | 23 | ### What is this about? 24 | 25 | Instead of storing tokens in the session, PSR7Csrf simply uses JWT tokens, 26 | which can be verified, signed and have a specific lifetime on their own. 27 | 28 | This storage-less approach prevents having to load tokens from a session 29 | or from a database, and simplifies the entire UI workflow: tokens are 30 | valid as long as their signature and expiration date holds. 31 | 32 | ### Installation 33 | 34 | ```sh 35 | composer require ocramius/psr7-csrf 36 | ``` 37 | 38 | ### Usage 39 | 40 | The simplest usage is based on defaults. It assumes that you have 41 | a configured PSR-7 compatible application that supports piping 42 | middlewares, and it also requires you to run [PSR7Session](https://github.com/Ocramius/PSR7Session). 43 | 44 | In a [`zendframework/zend-expressive`](https://github.com/zendframework/zend-expressive) 45 | application, the setup would look like the following: 46 | 47 | ```php 48 | $app = \Zend\Expressive\AppFactory::create(); 49 | 50 | $app->pipe(\PSR7Session\Http\SessionMiddleware::fromSymmetricKeyDefaults( 51 | 'mBC5v1sOKVvbdEitdSBenu59nfNfhwkedkJVNabosTw=', // replace this with a key of your own (see PSR7Session docs) 52 | 1200 // 20 minutes session duration 53 | )); 54 | 55 | $app->pipe(\PSR7Csrf\Factory::createDefaultCSRFCheckerMiddleware()); 56 | ``` 57 | 58 | This setup will require that any requests that are not `GET`, `HEAD` or 59 | `OPTIONS` contain a `csrf_token` in the request body parameters (JSON 60 | or URL-encoded). 61 | 62 | You can generate the CSRF token for any form like following: 63 | 64 | ```php 65 | $tokenGenerator = \PSR7Csrf\Factory::createDefaultTokenGenerator(); 66 | 67 | $app->get('/get', function ($request, $response) use ($tokenGenerator) { 68 | $response 69 | ->getBody() 70 | ->write( 71 | '
' 72 | . '' 73 | . '' 76 | . '
' 77 | ); 78 | 79 | return $response; 80 | }); 81 | 82 | $app->post('/post', function ($request, $response) { 83 | $response 84 | ->getBody() 85 | ->write('It works!'); 86 | 87 | return $response; 88 | }); 89 | ``` 90 | 91 | ### Examples 92 | 93 | ```sh 94 | composer install # install at the root of this package first! 95 | cd examples 96 | composer install 97 | php -S localhost:9999 index.php 98 | ``` 99 | 100 | Then try accessing `http://localhost:9999`: you should see a simple 101 | submission form. 102 | 103 | If you try modifying the submitted CSRF token (which is in a hidden 104 | form field), then the `POST` request will fail. 105 | 106 | ### Known limitations 107 | 108 | Please refer to the [known limitations of PSR7Session](https://github.com/Ocramius/PSR7Session/blob/master/docs/limitations.md). 109 | 110 | Also, this component does *NOT* prevent double-form-submissions: it 111 | merely prevents CSRF attacks from third parties. As long as the CSRF 112 | token is valid, it can be reused over multiple requests. 113 | 114 | ### Contributing 115 | 116 | Please refer to the [contributing notes](CONTRIBUTING.md). 117 | 118 | ### License 119 | 120 | This project is made public under the [MIT LICENSE](LICENSE). 121 | --------------------------------------------------------------------------------