HeaderSecurity::isValidName()   A
last analyzed

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 1
nc 1
nop 1
dl 0
loc 3
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\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
    public static function filter(string $value): string
49
    {
50
        $length = strlen($value);
51
        $string = '';
52
53
        for ($i = 0; $i < $length; $i++) {
54
            $ascii = ord($value[$i]);
55
56
            // Detect continuation sequences
57
            if ($ascii === 13) {
58
                $lf = ord($value[$i + 1]);
59
                $ws = ord($value[$i + 2]);
60
61
                if ($lf === 10 && in_array($ws, [9, 32], true)) {
62
                    $string .= $value[$i] . $value[$i + 1];
63
                    $i++;
64
                }
65
66
                continue;
67
            }
68
69
            // Non-visible, non-whitespace characters
70
            // 9 === horizontal tab
71
            // 32-126, 128-254 === visible
72
            // 127 === DEL
73
            // 255 === null byte
74
            if (
75
                ($ascii < 32 && $ascii !== 9)
76
                || $ascii === 127
77
                || $ascii > 254
78
            ) {
79
                continue;
80
            }
81
82
            $string .= $value[$i];
83
        }
84
85
        return $string;
86
    }
87
88
    /**
89
     * Assert a header value is valid.
90
     *
91
     * @throws InvalidArgumentException for invalid values
92
     */
93
    public static function assertValid(string $value): void
94
    {
95
        if (! self::isValid($value)) {
96
            throw new InvalidValueException(sprintf('"%s" is not valid header value', $value));
97
        }
98
    }
99
100
    /**
101
     * Validate a header value.
102
     * Per RFC 7230, only VISIBLE ASCII characters, spaces, and horizontal
103
     * tabs are allowed in values; header continuations MUST consist of
104
     * a single CRLF sequence followed by a space or horizontal tab.
105
     *
106
     * @see http://en.wikipedia.org/wiki/HTTP_response_splitting
107
     */
108
    public static function isValid(string $value): bool
109
    {
110
        // Look for:
111
        // \n not preceded by \r, OR
112
        // \r not followed by \n, OR
113
        // \r\n not followed by space or horizontal tab; these are all CRLF attacks
114
        if (preg_match("#(?:(?:(?<!\r)\n)|(?:\r(?!\n))|(?:\r\n(?![ \t])))#", $value)) {
115
            return false;
116
        }
117
118
        // Non-visible, non-whitespace characters
119
        // 9 === horizontal tab
120
        // 10 === line feed
121
        // 13 === carriage return
122
        // 32-126, 128-254 === visible
123
        // 127 === DEL (disallowed)
124
        // 255 === null byte (disallowed)
125
        if (preg_match('/[^\x09\x0a\x0d\x20-\x7E\x80-\xFE]/', $value)) {
126
            return false;
127
        }
128
129
        return true;
130
    }
131
132
    /**
133
     * Assert whether or not a header name is valid.
134
     *
135
     * @see http://tools.ietf.org/html/rfc7230#section-3.2
136
     *
137
     * @throws InvalidArgumentException
138
     */
139
    public static function assertValidName(string $name): void
140
    {
141
        if (! self::isValidName($name)) {
142
            throw new InvalidNameException(sprintf('"%s" is not valid header name', $name));
143
        }
144
    }
145
146
    public static function isValidName(string $name): bool
147
    {
148
        return (bool) preg_match('/^[a-zA-Z0-9\'`#$%&*+.^_|~!-]+$/D', $name);
149
    }
150
}
151