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