HeaderSecurity::filter()   B
last analyzed

Complexity

Conditions 9
Paths 5

Size

Total Lines 38
Code Lines 18

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 18
dl 0
loc 38
rs 8.0555
c 0
b 0
f 0
cc 9
nc 5
nop 1
1
<?php
2
3
declare(strict_types=1);
4
5
/*
6
 * This file is part of the Valkyrja Framework package.
7
 *
8
 * (c) Melech Mizrachi <[email protected]>
9
 *
10
 * For the full copyright and license information, please view the LICENSE
11
 * file that was distributed with this source code.
12
 */
13
14
namespace Valkyrja\Http\Message\Header\Security;
15
16
use Valkyrja\Http\Message\Header\Throwable\Exception\InvalidNameException;
17
use Valkyrja\Http\Message\Header\Throwable\Exception\InvalidValueException;
18
use Valkyrja\Http\Message\Throwable\Exception\InvalidArgumentException;
19
20
use function in_array;
21
use function ord;
22
use function preg_match;
23
use function sprintf;
24
use function strlen;
25
26
final class HeaderSecurity
27
{
28
    /**
29
     * Private constructor; non-instantiable.
30
     *
31
     * @codeCoverageIgnore
32
     */
33
    private function __construct()
34
    {
35
    }
36
37
    /**
38
     * Filter a header value.
39
     * Ensures CRLF header injection vectors are filtered.
40
     * Per RFC 7230, only VISIBLE ASCII characters, spaces, and horizontal
41
     * tabs are allowed in values; header continuations MUST consist of
42
     * a single CRLF sequence followed by a space or horizontal tab.
43
     * This method filters any values not allowed from the string, and is
44
     * lossy.
45
     *
46
     * @see http://en.wikipedia.org/wiki/HTTP_response_splitting
47
     *
48
     * @param string $value
49
     *
50
     * @return string
51
     */
52
    public static function filter(string $value): string
53
    {
54
        $length = strlen($value);
55
        $string = '';
56
57
        for ($i = 0; $i < $length; $i++) {
58
            $ascii = ord($value[$i]);
59
60
            // Detect continuation sequences
61
            if ($ascii === 13) {
62
                $lf = ord($value[$i + 1]);
63
                $ws = ord($value[$i + 2]);
64
65
                if ($lf === 10 && in_array($ws, [9, 32], true)) {
66
                    $string .= $value[$i] . $value[$i + 1];
67
                    $i++;
68
                }
69
70
                continue;
71
            }
72
73
            // Non-visible, non-whitespace characters
74
            // 9 === horizontal tab
75
            // 32-126, 128-254 === visible
76
            // 127 === DEL
77
            // 255 === null byte
78
            if (
79
                ($ascii < 32 && $ascii !== 9)
80
                || $ascii === 127
81
                || $ascii > 254
82
            ) {
83
                continue;
84
            }
85
86
            $string .= $value[$i];
87
        }
88
89
        return $string;
90
    }
91
92
    /**
93
     * Assert a header value is valid.
94
     *
95
     * @param string $value
96
     *
97
     * @throws InvalidArgumentException for invalid values
98
     *
99
     * @return void
100
     */
101
    public static function assertValid(string $value): void
102
    {
103
        if (! self::isValid($value)) {
104
            throw new InvalidValueException(sprintf('"%s" is not valid header value', $value));
105
        }
106
    }
107
108
    /**
109
     * Validate a header value.
110
     * Per RFC 7230, only VISIBLE ASCII characters, spaces, and horizontal
111
     * tabs are allowed in values; header continuations MUST consist of
112
     * a single CRLF sequence followed by a space or horizontal tab.
113
     *
114
     * @see http://en.wikipedia.org/wiki/HTTP_response_splitting
115
     *
116
     * @param string $value
117
     *
118
     * @return bool
119
     */
120
    public static function isValid(string $value): bool
121
    {
122
        // Look for:
123
        // \n not preceded by \r, OR
124
        // \r not followed by \n, OR
125
        // \r\n not followed by space or horizontal tab; these are all CRLF attacks
126
        if (preg_match("#(?:(?:(?<!\r)\n)|(?:\r(?!\n))|(?:\r\n(?![ \t])))#", $value)) {
127
            return false;
128
        }
129
130
        // Non-visible, non-whitespace characters
131
        // 9 === horizontal tab
132
        // 10 === line feed
133
        // 13 === carriage return
134
        // 32-126, 128-254 === visible
135
        // 127 === DEL (disallowed)
136
        // 255 === null byte (disallowed)
137
        if (preg_match('/[^\x09\x0a\x0d\x20-\x7E\x80-\xFE]/', $value)) {
138
            return false;
139
        }
140
141
        return true;
142
    }
143
144
    /**
145
     * Assert whether or not a header name is valid.
146
     *
147
     * @see http://tools.ietf.org/html/rfc7230#section-3.2
148
     *
149
     * @param string $name
150
     *
151
     * @throws InvalidArgumentException
152
     *
153
     * @return void
154
     */
155
    public static function assertValidName(string $name): void
156
    {
157
        if (! self::isValidName($name)) {
158
            throw new InvalidNameException(sprintf('"%s" is not valid header name', $name));
159
        }
160
    }
161
162
    public static function isValidName(string $name): bool
163
    {
164
        return (bool) preg_match('/^[a-zA-Z0-9\'`#$%&*+.^_|~!-]+$/D', $name);
165
    }
166
}
167