• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

codeigniter4 / CodeIgniter4 / 28438837637

30 Jun 2026 10:49AM UTC coverage: 88.308% (-0.002%) from 88.31%
28438837637

push

github

web-flow
Merge commit from fork

* fix: validate forwarded HTTPS headers against trusted proxies

* apply note about dual-stack servers

* apply method visibility change

37 of 39 new or added lines in 2 files covered. (94.87%)

22212 of 25153 relevant lines covered (88.31%)

212.36 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

97.39
/system/HTTP/RequestTrait.php
1
<?php
2

3
declare(strict_types=1);
4

5
/**
6
 * This file is part of CodeIgniter 4 framework.
7
 *
8
 * (c) CodeIgniter Foundation <admin@codeigniter.com>
9
 *
10
 * For the full copyright and license information, please view
11
 * the LICENSE file that was distributed with this source code.
12
 */
13

14
namespace CodeIgniter\HTTP;
15

16
use CodeIgniter\Exceptions\ConfigException;
17
use CodeIgniter\Validation\FormatRules;
18
use Config\App;
19

20
/**
21
 * Request Trait
22
 *
23
 * Additional methods to make a PSR-7 Request class
24
 * compliant with the framework's own RequestInterface.
25
 *
26
 * @see https://github.com/php-fig/http-message/blob/master/src/RequestInterface.php
27
 */
28
trait RequestTrait
29
{
30
    /**
31
     * Configuration settings.
32
     *
33
     * @var App
34
     */
35
    protected $config;
36

37
    /**
38
     * IP address of the current user.
39
     *
40
     * @var string
41
     *
42
     * @deprecated Will become private in a future release
43
     */
44
    protected $ipAddress = '';
45

46
    /**
47
     * Stores values we've retrieved from PHP globals.
48
     *
49
     * @var array{get?: array, post?: array, request?: array, cookie?: array, server?: array}
50
     *
51
     * @deprecated 4.7.0 Use the Superglobals service instead
52
     */
53
    protected $globals = [];
54

55
    /**
56
     * Gets the user's IP address.
57
     *
58
     * @return string IP address if it can be detected.
59
     *                If the IP address is not a valid IP address,
60
     *                then will return '0.0.0.0'.
61
     */
62
    public function getIPAddress(): string
63
    {
64
        if ($this->ipAddress !== '') {
79✔
65
            return $this->ipAddress;
2✔
66
        }
67

68
        $ipValidator = [
79✔
69
            new FormatRules(),
79✔
70
            'valid_ip',
79✔
71
        ];
79✔
72

73
        $proxyIPs = $this->config->proxyIPs;
79✔
74

75
        if (! empty($proxyIPs) && (! is_array($proxyIPs) || is_int(array_key_first($proxyIPs)))) {
79✔
76
            throw new ConfigException(
1✔
77
                'You must set an array with Proxy IP address key and HTTP header name value in Config\App::$proxyIPs.',
1✔
78
            );
1✔
79
        }
80

81
        $this->ipAddress = $this->getServer('REMOTE_ADDR');
78✔
82

83
        // If this is a CLI request, $this->ipAddress is null.
84
        if ($this->ipAddress === null) {
78✔
85
            return $this->ipAddress = '0.0.0.0';
50✔
86
        }
87

88
        // @TODO Extract all this IP address logic to another class.
89
        foreach ($proxyIPs as $proxyIP => $header) {
28✔
90
            if ($this->checkIPAgainstProxy($this->ipAddress, (string) $proxyIP)) {
26✔
91
                $spoof = $this->getClientIP($header);
11✔
92

93
                if ($spoof !== null) {
11✔
94
                    $this->ipAddress = $spoof;
8✔
95
                    break;
8✔
96
                }
97
            }
98
        }
99

100
        if (! $ipValidator($this->ipAddress)) {
28✔
101
            return $this->ipAddress = '0.0.0.0';
×
102
        }
103

104
        return $this->ipAddress;
28✔
105
    }
106

107
    /**
108
     * Gets the client IP address from the HTTP header.
109
     */
110
    private function getClientIP(string $header): ?string
111
    {
112
        $ipValidator = [
11✔
113
            new FormatRules(),
11✔
114
            'valid_ip',
11✔
115
        ];
11✔
116
        $spoof     = null;
11✔
117
        $headerObj = $this->header($header);
11✔
118

119
        if ($headerObj !== null) {
11✔
120
            $spoof = $headerObj->getValue();
11✔
121

122
            // Some proxies typically list the whole chain of IP
123
            // addresses through which the client has reached us.
124
            // e.g. client_ip, proxy_ip1, proxy_ip2, etc.
125
            sscanf($spoof, '%[^,]', $spoof);
11✔
126

127
            if (! $ipValidator($spoof)) {
11✔
128
                $spoof = null;
3✔
129
            }
130
        }
131

132
        return $spoof;
11✔
133
    }
134

135
    /**
136
     * Checks if the request comes from one of the trusted proxies
137
     * configured in Config\App::$proxyIPs.
138
     */
139
    protected function isFromTrustedProxy(): bool
140
    {
141
        $proxyIPs = $this->config->proxyIPs;
20✔
142

143
        if (! is_array($proxyIPs) || $proxyIPs === []) {
20✔
144
            return false;
5✔
145
        }
146

147
        $remoteAddr = $this->getServer('REMOTE_ADDR');
15✔
148

149
        if (! is_string($remoteAddr) || $remoteAddr === '') {
15✔
NEW
150
            return false;
×
151
        }
152

153
        foreach (array_keys($proxyIPs) as $proxyIP) {
15✔
154
            if ($this->checkIPAgainstProxy($remoteAddr, (string) $proxyIP)) {
15✔
155
                return true;
6✔
156
            }
157
        }
158

159
        return false;
9✔
160
    }
161

162
    /**
163
     * Checks if the given IP address matches the trusted proxy entry,
164
     * which may be a single IP address or a subnet in CIDR notation.
165
     * Supports both IPv4 and IPv6.
166
     */
167
    private function checkIPAgainstProxy(string $ip, string $proxyIP): bool
168
    {
169
        $maskLength = null;
41✔
170

171
        if (str_contains($proxyIP, '/')) {
41✔
172
            [$proxyIP, $mask] = explode('/', $proxyIP, 2);
28✔
173

174
            if ($mask === '' || ! ctype_digit($mask)) {
28✔
175
                return false;
6✔
176
            }
177

178
            $maskLength = (int) $mask;
22✔
179
        }
180

181
        $binaryIP    = inet_pton($ip);
35✔
182
        $binaryProxy = inet_pton($proxyIP);
35✔
183

184
        if ($binaryIP === false || $binaryProxy === false) {
35✔
185
            return false;
2✔
186
        }
187

188
        // If the proxy entry doesn't match the IP protocol - no match
189
        if (strlen($binaryIP) !== strlen($binaryProxy)) {
33✔
190
            return false;
2✔
191
        }
192

193
        if ($maskLength === null) {
32✔
194
            return $binaryIP === $binaryProxy;
15✔
195
        }
196

197
        if ($maskLength > strlen($binaryIP) * 8) {
21✔
198
            return false;
4✔
199
        }
200

201
        if ($maskLength === 0) {
17✔
NEW
202
            return true;
×
203
        }
204

205
        $fullBytes     = intdiv($maskLength, 8);
17✔
206
        $remainingBits = $maskLength % 8;
17✔
207

208
        if ($fullBytes > 0 && strncmp($binaryIP, $binaryProxy, $fullBytes) !== 0) {
17✔
209
            return false;
9✔
210
        }
211

212
        if ($remainingBits > 0) {
8✔
213
            $bitmask = 0xFF & (0xFF << (8 - $remainingBits));
2✔
214

215
            return (ord($binaryIP[$fullBytes]) & $bitmask) === (ord($binaryProxy[$fullBytes]) & $bitmask);
2✔
216
        }
217

218
        return true;
6✔
219
    }
220

221
    /**
222
     * Fetch an item from the $_SERVER array.
223
     *
224
     * @param array|string|null $index  Index for item to be fetched from $_SERVER
225
     * @param int|null          $filter A filter name to be applied
226
     * @param array|int|null    $flags
227
     *
228
     * @return mixed
229
     */
230
    public function getServer($index = null, $filter = null, $flags = null)
231
    {
232
        return $this->fetchGlobal('server', $index, $filter, $flags);
1,844✔
233
    }
234

235
    /**
236
     * Fetch an item from the $_ENV array.
237
     *
238
     * @param array|string|null $index  Index for item to be fetched from $_ENV
239
     * @param int|null          $filter A filter name to be applied
240
     * @param array|int|null    $flags
241
     *
242
     * @return mixed
243
     *
244
     * @deprecated 4.4.4 This method does not work from the beginning. Use `env()`.
245
     */
246
    public function getEnv($index = null, $filter = null, $flags = null)
247
    {
248
        // @phpstan-ignore-next-line
249
        return $this->fetchGlobal('env', $index, $filter, $flags);
250
    }
251

252
    /**
253
     * Allows manually setting the value of PHP global, like $_GET, $_POST, etc.
254
     *
255
     * @param 'cookie'|'get'|'post'|'request'|'server' $name  Superglobal name (lowercase)
256
     * @param mixed                                    $value
257
     *
258
     * @return $this
259
     */
260
    public function setGlobal(string $name, $value)
261
    {
262
        // Keep BC with $globals array
263
        $this->globals[$name] = $value;
122✔
264

265
        // Also update Superglobals via service
266
        service('superglobals')->setGlobalArray($name, $value);
122✔
267

268
        return $this;
122✔
269
    }
270

271
    /**
272
     * Fetches one or more items from a global, like cookies, get, post, etc.
273
     * Can optionally filter the input when you retrieve it by passing in
274
     * a filter.
275
     *
276
     * If $type is an array, it must conform to the input allowed by the
277
     * filter_input_array method.
278
     *
279
     * http://php.net/manual/en/filter.filters.sanitize.php
280
     *
281
     * @param 'cookie'|'get'|'post'|'request'|'server' $name   Superglobal name (lowercase)
282
     * @param array|int|string|null                    $index
283
     * @param int|null                                 $filter Filter constant
284
     * @param array|int|null                           $flags  Options
285
     *
286
     * @return mixed
287
     */
288
    public function fetchGlobal(string $name, $index = null, ?int $filter = null, $flags = null)
289
    {
290
        if (! isset($this->globals[$name])) {
1,882✔
291
            $this->populateGlobals($name);
1,731✔
292
        }
293

294
        // Null filters cause null values to return.
295
        $filter ??= FILTER_UNSAFE_RAW;
1,882✔
296
        $flags = is_array($flags) ? $flags : (is_numeric($flags) ? (int) $flags : 0);
1,882✔
297

298
        // Return all values when $index is null
299
        if ($index === null) {
1,882✔
300
            $values = [];
99✔
301

302
            foreach ($this->globals[$name] as $key => $value) {
99✔
303
                $values[$key] = is_array($value)
57✔
304
                    ? $this->fetchGlobal($name, $key, $filter, $flags)
17✔
305
                    : filter_var($value, $filter, $flags);
45✔
306
            }
307

308
            return $values;
99✔
309
        }
310

311
        // allow fetching multiple keys at once
312
        if (is_array($index)) {
1,882✔
313
            $output = [];
8✔
314

315
            foreach ($index as $key) {
8✔
316
                $output[$key] = $this->fetchGlobal($name, $key, $filter, $flags);
8✔
317
            }
318

319
            return $output;
8✔
320
        }
321

322
        // Does the index contain array notation?
323
        if (is_string($index) && ($count = preg_match_all('/(?:^[^\[]+)|\[[^]]*\]/', $index, $matches)) > 1) {
1,882✔
324
            $value = $this->globals[$name];
11✔
325

326
            for ($i = 0; $i < $count; $i++) {
11✔
327
                $key = trim($matches[0][$i], '[]');
11✔
328

329
                if ($key === '') { // Empty notation will return the value as array
11✔
330
                    break;
2✔
331
                }
332

333
                if (isset($value[$key])) {
11✔
334
                    $value = $value[$key];
11✔
335
                } else {
336
                    return null;
2✔
337
                }
338
            }
339
        }
340

341
        if (! isset($value)) {
1,882✔
342
            $value = $this->globals[$name][$index] ?? null;
1,882✔
343
        }
344

345
        if (is_array($value)
1,882✔
346
            && (
347
                $filter !== FILTER_UNSAFE_RAW
1,882✔
348
                || (
1,882✔
349
                    (is_numeric($flags) && $flags !== 0)
1,882✔
350
                    || is_array($flags) && $flags !== []
1,882✔
351
                )
1,882✔
352
            )
353
        ) {
354
            // Iterate over array and append filter and flags
355
            array_walk_recursive($value, static function (&$val) use ($filter, $flags): void {
4✔
356
                $val = filter_var($val, $filter, $flags);
4✔
357
            });
4✔
358

359
            return $value;
4✔
360
        }
361

362
        // Cannot filter these types of data automatically...
363
        if (is_array($value) || is_object($value) || $value === null) {
1,882✔
364
            return $value;
1,493✔
365
        }
366

367
        return filter_var($value, $filter, $flags);
687✔
368
    }
369

370
    /**
371
     * Saves a copy of the current state of one of several PHP globals,
372
     * so we can retrieve them later.
373
     *
374
     * @param 'cookie'|'get'|'post'|'request'|'server' $name Superglobal name (lowercase)
375
     *
376
     * @return void
377
     *
378
     * @deprecated 4.7.0 No longer needs to be called explicitly. Used internally to maintain BC with $globals.
379
     */
380
    protected function populateGlobals(string $name)
381
    {
382
        if (! isset($this->globals[$name])) {
383
            $this->globals[$name] = [];
384
        }
385

386
        // Get data from Superglobals service instead of direct access
387
        $this->globals[$name] = service('superglobals')->getGlobalArray($name);
388
    }
389
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc