Completed
Pull Request — master (#1)
by David
05:31
created

CsrfHeaderCheckMiddleware   A

Complexity

Total Complexity 14

Size/Duplication

Total Lines 88
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 3

Importance

Changes 0
Metric Value
wmc 14
lcom 1
cbo 3
dl 0
loc 88
rs 10
c 0
b 0
f 0

6 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 4 1
A process() 0 12 3
A getSourceOrigin() 0 14 3
A getTargetOrigin() 0 12 3
A removePortFromHost() 0 4 1
A getHeaderLine() 0 11 3
1
<?php
2
declare(strict_types=1);
3
4
namespace TheCodingMachine\Middlewares;
5
6
use Interop\Http\ServerMiddleware\DelegateInterface;
7
use Interop\Http\ServerMiddleware\MiddlewareInterface;
8
use Psr\Http\Message\ResponseInterface;
9
use Psr\Http\Message\ServerRequestInterface;
10
use TheCodingMachine\Middlewares\SafeRequests\IsSafeHttpRequestInterface;
11
12
/**
13
 * This class will check that all POST/PUT/DELETE... requests and verify that the "Origin" of the request is your own website.
14
 */
15
final class CsrfHeaderCheckMiddleware implements MiddlewareInterface
16
{
17
    /**
18
     * @var IsSafeHttpRequestInterface
19
     */
20
    private $isSafeHttpRequest;
21
22
    public function __construct(IsSafeHttpRequestInterface $isSafeHttpRequest)
23
    {
24
        $this->isSafeHttpRequest = $isSafeHttpRequest;
25
    }
26
27
    /**
28
     * Process an incoming server request and return a response, optionally delegating
29
     * to the next middleware component to create the response.
30
     *
31
     * @param ServerRequestInterface $request
32
     * @param DelegateInterface $delegate
33
     * @return ResponseInterface
34
     * @throws CsrfHeaderCheckMiddlewareException
35
     */
36
    public function process(ServerRequestInterface $request, DelegateInterface $delegate)
37
    {
38
        $isSafeHttpRequest = $this->isSafeHttpRequest;
39
        if (!$isSafeHttpRequest($request)) {
40
            $source = $this->getSourceOrigin($request);
41
            $target = $this->getTargetOrigin($request);
42
            if ($source !== $target) {
43
                throw new CsrfHeaderCheckMiddlewareException('Potential CSRF attack stopped. Source origin and target origin do not match.');
44
            }
45
        }
46
        return $delegate->process($request);
47
    }
48
49
    private function getSourceOrigin(ServerRequestInterface $request): string
50
    {
51
        $source = $this->getHeaderLine($request, 'ORIGIN');
52
        if (null !== $source) {
53
            return parse_url($source, PHP_URL_HOST);
54
        }
55
56
        $referrer = $this->getHeaderLine($request, 'REFERER');
57
        if (null === $referrer) {
58
            throw new CsrfHeaderCheckMiddlewareException('Could not find neither the ORIGIN header nor the REFERER header in the HTTP request.');
59
        }
60
61
        return parse_url($referrer, PHP_URL_HOST);
62
    }
63
64
    private function getTargetOrigin(ServerRequestInterface $request): string
65
    {
66
        $host = $this->getHeaderLine($request, 'X-FORWARDED-HOST');
67
        if (null === $host) {
68
            $host = $this->getHeaderLine($request, 'HOST');
69
        }
70
71
        if (null === $host) {
72
            throw new CsrfHeaderCheckMiddlewareException('Could not find the HOST header in the HTTP request.');
73
        }
74
        return $this->removePortFromHost($host);
75
    }
76
77
    private function removePortFromHost(string $host)
78
    {
79
        return parse_url('http://'.$host, PHP_URL_HOST);
80
    }
81
82
    /**
83
     * Returns the header, throws an exception if the header is specified more that one in the request.
84
     * Returns null if nothing found.
85
     *
86
     * @param ServerRequestInterface $request
87
     * @param string $header
88
     * @return string|null
89
     * @throws CsrfHeaderCheckMiddlewareException
90
     */
91
    private function getHeaderLine(ServerRequestInterface $request, string $header)
92
    {
93
        $values = $request->getHeader($header);
94
        if (count($values) > 1) {
95
            throw new CsrfHeaderCheckMiddlewareException("Unexpected request: more than one $header header sent.");
96
        }
97
        if (count($values) === 1) {
98
            return $values[0];
99
        }
100
        return null;
101
    }
102
}
103