HeaderSecurity::__construct()   A
last analyzed

Complexity

Conditions 1
Paths 1

Size

Total Lines 2
Code Lines 0

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 0
nc 1
nop 0
dl 0
loc 2
rs 10
c 0
b 0
f 0
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 (($ascii < 32 && $ascii !== 9)
84
                || $ascii === 127
85
                || $ascii > 254
86
            ) {
87
                continue;
88
            }
89
90
            $string .= $value[$i];
91
        }
92
93
        return $string;
94
    }
95
96
    /**
97
     * Assert a header value is valid.
98
     *
99
     * @param string $value
100
     *
101
     * @throws InvalidArgumentException for invalid values
102
     *
103
     * @return void
104
     */
105
    public static function assertValid(string $value): void
106
    {
107
        if (! self::isValid($value)) {
108
            throw new InvalidValueException(sprintf('"%s" is not valid header value', $value));
109
        }
110
    }
111
112
    /**
113
     * Validate a header value.
114
     * Per RFC 7230, only VISIBLE ASCII characters, spaces, and horizontal
115
     * tabs are allowed in values; header continuations MUST consist of
116
     * a single CRLF sequence followed by a space or horizontal tab.
117
     *
118
     * @see http://en.wikipedia.org/wiki/HTTP_response_splitting
119
     *
120
     * @param string $value
121
     *
122
     * @return bool
123
     */
124
    public static function isValid(string $value): bool
125
    {
126
        // Look for:
127
        // \n not preceded by \r, OR
128
        // \r not followed by \n, OR
129
        // \r\n not followed by space or horizontal tab; these are all CRLF attacks
130
        if (preg_match("#(?:(?:(?<!\r)\n)|(?:\r(?!\n))|(?:\r\n(?![ \t])))#", $value)) {
131
            return false;
132
        }
133
134
        // Non-visible, non-whitespace characters
135
        // 9 === horizontal tab
136
        // 10 === line feed
137
        // 13 === carriage return
138
        // 32-126, 128-254 === visible
139
        // 127 === DEL (disallowed)
140
        // 255 === null byte (disallowed)
141
        if (preg_match('/[^\x09\x0a\x0d\x20-\x7E\x80-\xFE]/', $value)) {
142
            return false;
143
        }
144
145
        return true;
146
    }
147
148
    /**
149
     * Assert whether or not a header name is valid.
150
     *
151
     * @see http://tools.ietf.org/html/rfc7230#section-3.2
152
     *
153
     * @param string $name
154
     *
155
     * @throws InvalidArgumentException
156
     *
157
     * @return void
158
     */
159
    public static function assertValidName(string $name): void
160
    {
161
        if (! self::isValidName($name)) {
162
            throw new InvalidNameException(sprintf('"%s" is not valid header name', $name));
163
        }
164
    }
165
166
    public static function isValidName(string $name): bool
167
    {
168
        return (bool) preg_match('/^[a-zA-Z0-9\'`#$%&*+.^_|~!-]+$/D', $name);
169
    }
170
}
171