├── 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 | [](https://travis-ci.org/Ocramius/PSR7Csrf)
4 | [](https://scrutinizer-ci.com/g/Ocramius/PSR7Csrf/?branch=master)
5 | [](https://scrutinizer-ci.com/g/Ocramius/PSR7Csrf/?branch=master)
6 | [](https://packagist.org/packages/ocramius/psr7-csrf)
7 | [](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 | '
'
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 |
--------------------------------------------------------------------------------