InvalidSymfonyHttpMethodOverrideFilter   A
last analyzed

Complexity

Total Complexity 6

Size/Duplication

Total Lines 71
Duplicated Lines 0 %

Test Coverage

Coverage 92.86%

Importance

Changes 0
Metric Value
wmc 6
eloc 35
dl 0
loc 71
ccs 26
cts 28
cp 0.9286
rs 10
c 0
b 0
f 0

1 Method

Rating   Name   Duplication   Size   Complexity  
B __invoke() 0 53 6
1
<?php
2
3
declare(strict_types=1);
4
5
namespace DanBettles\Defence\Filter;
6
7
use DanBettles\Defence\Envelope;
8
use Symfony\Component\HttpFoundation\Request;
9
10
use function array_diff;
11
use function array_map;
12
use function implode;
13
use function is_string;
14
use function sprintf;
15
use function strtoupper;
16
17
use const false;
18
use const null;
19
use const true;
20
21
/**
22
 * Using the Symfony request object it is possible to simulate exotic HTTP methods (e.g. `PUT`) by making a `POST`
23
 * request and specifying the intended method in a special parameter or in a special header.
24
 *
25
 * We've seen requests that have tried to exploit this feature.  It's an attack vector that's easy to detect, so we
26
 * provided this simple filter, which will return `true` if the request-method override looks at all dodgy.
27
 *
28
 * The approach is very simple: if a request looks even vaguely dodgy then the filter will reject it, even if Symfony
29
 * wouldn't touch it anyway.
30
 */
31
class InvalidSymfonyHttpMethodOverrideFilter extends AbstractFilter
32
{
33
    /**
34
     * @var string[]
35
     */
36
    public const VALID_METHODS = [
37
        Request::METHOD_HEAD,
38
        Request::METHOD_GET,
39
        Request::METHOD_POST,
40
        Request::METHOD_PUT,
41
        Request::METHOD_PATCH,
42
        Request::METHOD_DELETE,
43
        Request::METHOD_PURGE,
44
        Request::METHOD_OPTIONS,
45
        Request::METHOD_TRACE,
46
        Request::METHOD_CONNECT,
47
    ];
48
49 514
    public function __invoke(Envelope $envelope): bool
50
    {
51 514
        $request = $envelope->getRequest();
52
53 514
        $allMethodOverrides = [
54 514
            $request->headers->get('X-HTTP-METHOD-OVERRIDE'),
55 514
            $request->request->get('_method'),
56 514
            $request->query->get('_method'),
57 514
        ];
58
59 514
        $filteredMethodOverrides = [];
60
61 514
        foreach ($allMethodOverrides as $methodOverride) {
62 514
            if (null === $methodOverride) {
63 514
                continue;
64
            }
65
66 504
            if (!is_string($methodOverride)) {
67
                $this->envelopeAddLogEntry($envelope, 'The type of the request-method override is invalid');
68
69
                return true;
70
            }
71
72 504
            $filteredMethodOverrides[] = strtoupper($methodOverride);
73
        }
74
75 514
        if (!$filteredMethodOverrides) {
76
            // Nothing at all to worry about
77 10
            return false;
78
        }
79
80
        // (Request-method override present)
81
82 504
        $invalidMethodOverrides = array_diff($filteredMethodOverrides, self::VALID_METHODS);
83
84
        // Reject the request if any request-method override, found anywhere, is invalid
85 504
        if (!empty($invalidMethodOverrides)) {
86 174
            $this->envelopeAddLogEntry($envelope, sprintf(
87 174
                'The request contains invalid request-method overrides: %s',
88 174
                implode(
89 174
                    ', ',
90 174
                    array_map(fn ($invalidMethod) => "`{$invalidMethod}`", $invalidMethodOverrides)
91 174
                )
92 174
            ));
93
94 174
            return true;
95
        }
96
97
        // (Valid request-method override present)
98
99
        // A request-method override should accompany only `POST` requests, so if the method is something else and an
100
        // override is present then the request is suspicious
101 330
        return Request::METHOD_POST !== $request->getRealMethod();
102
    }
103
}
104