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

codeigniter4 / CodeIgniter4 / 27231291599

09 Jun 2026 07:43PM UTC coverage: 88.235% (+0.002%) from 88.233%
27231291599

Pull #10294

github

web-flow
Merge 5775830aa into 1bee8837c
Pull Request #10294: fix: protect internal session variables against user overwrite

4 of 4 new or added lines in 1 file covered. (100.0%)

11 existing lines in 1 file now uncovered.

22147 of 25100 relevant lines covered (88.24%)

210.94 hits per line

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

80.37
/system/Session/Session.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\Session;
15

16
use CodeIgniter\Cookie\Cookie;
17
use CodeIgniter\I18n\Time;
18
use Config\Cookie as CookieConfig;
19
use Config\Session as SessionConfig;
20
use Psr\Log\LoggerAwareTrait;
21
use SessionHandlerInterface;
22

23
/**
24
 * Implementation of CodeIgniter session container.
25
 *
26
 * Session configuration is done through session variables and cookie related
27
 * variables in `Сonfig\Session`.
28
 *
29
 * @property string $session_id
30
 *
31
 * @see \CodeIgniter\Session\SessionTest
32
 */
33
class Session implements SessionInterface
34
{
35
    use LoggerAwareTrait;
36

37
    /**
38
     * Instance of the driver to use.
39
     *
40
     * @var SessionHandlerInterface
41
     */
42
    protected $driver;
43

44
    /**
45
     * The session cookie instance.
46
     *
47
     * @var Cookie
48
     */
49
    protected $cookie;
50

51
    /**
52
     * Session ID regex expression.
53
     *
54
     * @var string
55
     */
56
    protected $sidRegexp;
57

58
    protected SessionConfig $config;
59

60
    /**
61
     * Extract configuration settings and save them here.
62
     */
63
    public function __construct(SessionHandlerInterface $driver, SessionConfig $config)
64
    {
65
        $this->driver = $driver;
7,304✔
66
        $this->config = $config;
7,304✔
67
        $cookie       = config(CookieConfig::class);
7,304✔
68

69
        $this->cookie = (new Cookie($this->config->cookieName, '', [
7,304✔
70
            'expires'  => $this->config->expiration === 0 ? 0 : Time::now()->getTimestamp() + $this->config->expiration,
7,304✔
71
            'path'     => $cookie->path,
7,304✔
72
            'domain'   => $cookie->domain,
7,304✔
73
            'secure'   => $cookie->secure,
7,304✔
74
            'httponly' => true, // for security
7,304✔
75
            'samesite' => $cookie->samesite,
7,304✔
76
            'raw'      => $cookie->raw,
7,304✔
77
        ]))->withPrefix(''); // Cookie prefix should be ignored.
7,304✔
78

79
        helper('array');
7,304✔
80
    }
81

82
    /**
83
     * Initialize the session container and starts up the session.
84
     *
85
     * @return $this|null
86
     */
87
    public function start()
88
    {
89
        if (is_cli() && ENVIRONMENT !== 'testing') {
94✔
90
            // @codeCoverageIgnoreStart
91
            $this->logger->debug('Session: Initialization under CLI aborted.');
92

93
            return null;
94
            // @codeCoverageIgnoreEnd
95
        }
96

97
        if ((bool) ini_get('session.auto_start')) {
94✔
98
            $this->logger->error('Session: session.auto_start is enabled in php.ini. Aborting.');
×
99

100
            return null;
×
101
        }
102

103
        if (session_status() === PHP_SESSION_ACTIVE) {
94✔
104
            $this->logger->warning('Session: Sessions is enabled, and one exists. Please don\'t $session->start();');
×
105

106
            return null;
×
107
        }
108

109
        $this->configure();
94✔
110
        $this->setSaveHandler();
94✔
111

112
        // Sanitize the cookie, because apparently PHP doesn't do that for userspace handlers
113
        if (
114
            isset($_COOKIE[$this->config->cookieName])
94✔
115
            && (! is_string($_COOKIE[$this->config->cookieName]) || preg_match('#\A' . $this->sidRegexp . '\z#', $_COOKIE[$this->config->cookieName]) !== 1)
94✔
116
        ) {
117
            unset($_COOKIE[$this->config->cookieName]);
×
118
        }
119

120
        $this->startSession();
94✔
121

122
        // Is session ID auto-regeneration configured? (ignoring ajax requests)
123
        $requestedWith = service('superglobals')->server('HTTP_X_REQUESTED_WITH');
94✔
124
        if (($requestedWith === null || strtolower($requestedWith) !== 'xmlhttprequest')
94✔
125
            && ($regenerateTime = $this->config->timeToUpdate) > 0
94✔
126
        ) {
127
            if (! isset($_SESSION['__ci_last_regenerate'])) {
92✔
128
                $_SESSION['__ci_last_regenerate'] = Time::now()->getTimestamp();
91✔
129
            } elseif ($_SESSION['__ci_last_regenerate'] < (Time::now()->getTimestamp() - $regenerateTime)) {
3✔
130
                $this->regenerate($this->config->regenerateDestroy);
1✔
131
            }
132
        }
133
        // Another work-around ... PHP doesn't seem to send the session cookie
134
        // unless it is being currently created or regenerated
135
        elseif (isset($_COOKIE[$this->config->cookieName]) && $_COOKIE[$this->config->cookieName] === session_id()) {
2✔
136
            $this->setCookie();
×
137
        }
138

139
        $this->initVars();
94✔
140
        $this->logger->debug("Session: Class initialized using '" . $this->config->driver . "' driver.");
94✔
141

142
        return $this;
94✔
143
    }
144

145
    /**
146
     * Configuration.
147
     *
148
     * Handle input binds and configuration defaults.
149
     *
150
     * @return void
151
     */
152
    protected function configure()
153
    {
154
        ini_set('session.name', $this->config->cookieName);
94✔
155

156
        $sameSite = $this->cookie->getSameSite() === ''
94✔
157
            ? ucfirst(Cookie::SAMESITE_LAX)
×
158
            : $this->cookie->getSameSite();
94✔
159

160
        $params = [
94✔
161
            'lifetime' => $this->config->expiration,
94✔
162
            'path'     => $this->cookie->getPath(),
94✔
163
            'domain'   => $this->cookie->getDomain(),
94✔
164
            'secure'   => $this->cookie->isSecure(),
94✔
165
            'httponly' => true, // HTTP only; Yes, this is intentional and not configurable for security reasons.
94✔
166
            'samesite' => $sameSite,
94✔
167
        ];
94✔
168

169
        ini_set('session.cookie_samesite', $sameSite);
94✔
170
        session_set_cookie_params($params);
94✔
171

172
        if ($this->config->expiration > 0) {
94✔
173
            ini_set('session.gc_maxlifetime', (string) $this->config->expiration);
93✔
174
        }
175

176
        if ($this->config->savePath !== '') {
94✔
177
            ini_set('session.save_path', $this->config->savePath);
48✔
178
        }
179

180
        // Security is king
181
        ini_set('session.use_trans_sid', '0');
94✔
182
        ini_set('session.use_strict_mode', '1');
94✔
183
        ini_set('session.use_cookies', '1');
94✔
184
        ini_set('session.use_only_cookies', '1');
94✔
185

186
        $this->configureSidLength();
94✔
187
    }
188

189
    /**
190
     * Configure session ID length.
191
     *
192
     * To make life easier, we force the PHP defaults. Because PHP9 forces them.
193
     * See https://wiki.php.net/rfc/deprecations_php_8_4#sessionsid_length_and_sessionsid_bits_per_character
194
     *
195
     * @return void
196
     */
197
    protected function configureSidLength()
198
    {
199
        $bitsPerCharacter = (int) ini_get('session.sid_bits_per_character');
94✔
200
        $sidLength        = (int) ini_get('session.sid_length');
94✔
201

202
        // We force the PHP defaults.
203
        if (PHP_VERSION_ID < 90000) {
94✔
204
            if ($bitsPerCharacter !== 4) {
94✔
205
                ini_set('session.sid_bits_per_character', '4');
×
206
            }
207
            if ($sidLength !== 32) {
94✔
208
                ini_set('session.sid_length', '32');
×
209
            }
210
        }
211

212
        $this->sidRegexp = '[0-9a-f]{32}';
94✔
213
    }
214

215
    /**
216
     * Handle temporary variables.
217
     *
218
     * Clears old "flash" data, marks the new one for deletion and handles
219
     * "temp" data deletion.
220
     *
221
     * @return void
222
     */
223
    protected function initVars()
224
    {
225
        if (! isset($_SESSION['__ci_vars'])) {
94✔
226
            return;
94✔
227
        }
228

229
        $currentTime = Time::now()->getTimestamp();
2✔
230

231
        foreach ($_SESSION['__ci_vars'] as $key => &$value) {
2✔
232
            if ($value === 'new') {
2✔
233
                $_SESSION['__ci_vars'][$key] = 'old';
2✔
234
            }
235
            // DO NOT move this above the 'new' check!
236
            elseif ($value === 'old' || $value < $currentTime) {
1✔
237
                unset($_SESSION[$key], $_SESSION['__ci_vars'][$key]);
1✔
238
            }
239
        }
240

241
        if ($_SESSION['__ci_vars'] === []) {
2✔
242
            unset($_SESSION['__ci_vars']);
1✔
243
        }
244
    }
245

246
    public function regenerate(bool $destroy = false)
247
    {
248
        $_SESSION['__ci_last_regenerate'] = Time::now()->getTimestamp();
×
249
        session_regenerate_id($destroy);
×
250

251
        $this->removeOldSessionCookie();
×
252
    }
253

254
    private function removeOldSessionCookie(): void
255
    {
256
        $response              = service('response');
×
257
        $cookieStoreInResponse = $response->getCookieStore();
×
258

259
        if (! $cookieStoreInResponse->has($this->config->cookieName)) {
×
260
            return;
×
261
        }
262

263
        // CookieStore is immutable.
264
        $newCookieStore = $cookieStoreInResponse->remove($this->config->cookieName);
×
265

266
        // But clear() method clears cookies in the object (not immutable).
267
        $cookieStoreInResponse->clear();
×
268

269
        foreach ($newCookieStore as $cookie) {
×
270
            $response->setCookie($cookie);
×
271
        }
272
    }
273

274
    public function destroy()
275
    {
276
        if (ENVIRONMENT === 'testing') {
×
277
            return;
×
278
        }
279

280
        session_destroy();
×
281
    }
282

283
    /**
284
     * Writes session data and close the current session.
285
     *
286
     * @return void
287
     */
288
    public function close()
289
    {
290
        if (ENVIRONMENT === 'testing') {
×
291
            return;
×
292
        }
293

294
        session_write_close();
×
295
    }
296

297
    public function set($data, $value = null)
298
    {
299
        $data = is_array($data) ? $data : [$data => $value];
128✔
300

301
        if (array_is_list($data)) {
128✔
302
            $data = array_fill_keys($data, null);
1✔
303
        }
304

305
        foreach ($data as $sessionKey => $sessionValue) {
128✔
306
            if (is_string($sessionKey) && str_starts_with($sessionKey, '__ci_')) {
128✔
307
                continue;
1✔
308
            }
309

310
            $_SESSION[$sessionKey] = $sessionValue;
127✔
311
        }
312
    }
313

314
    public function get(?string $key = null)
315
    {
316
        if (! isset($_SESSION) || $_SESSION === []) {
69✔
317
            return $key === null ? [] : null;
12✔
318
        }
319

320
        $key ??= '';
59✔
321

322
        if ($key !== '') {
59✔
323
            return $_SESSION[$key] ?? dot_array_search($key, $_SESSION);
58✔
324
        }
325

326
        $userdata = [];
1✔
327
        $exclude  = array_merge(['__ci_vars'], $this->getFlashKeys(), $this->getTempKeys());
1✔
328

329
        foreach (array_keys($_SESSION) as $key) {
1✔
330
            if (! in_array($key, $exclude, true)) {
1✔
331
                $userdata[$key] = $_SESSION[$key];
1✔
332
            }
333
        }
334

335
        return $userdata;
1✔
336
    }
337

338
    public function has(string $key): bool
339
    {
340
        return isset($_SESSION[$key]);
40✔
341
    }
342

343
    /**
344
     * Push new value onto session value that is array.
345
     *
346
     * @param string               $key  Identifier of the session property we are interested in.
347
     * @param array<string, mixed> $data value to be pushed to existing session key.
348
     *
349
     * @return void
350
     */
351
    public function push(string $key, array $data)
352
    {
353
        if ($this->has($key) && is_array($value = $this->get($key))) {
1✔
354
            $this->set($key, array_merge($value, $data));
1✔
355
        }
356
    }
357

358
    public function remove($key)
359
    {
360
        $key = is_array($key) ? $key : [$key];
2✔
361

362
        foreach ($key as $k) {
2✔
363
            unset($_SESSION[$k]);
2✔
364
        }
365
    }
366

367
    /**
368
     * Magic method to set variables in the session by simply calling
369
     *  $session->foo = 'bar';
370
     *
371
     * @param mixed $value
372
     *
373
     * @return void
374
     */
375
    public function __set(string $key, $value)
376
    {
377
        if (str_starts_with($key, '__ci_')) {
2✔
378
            return;
1✔
379
        }
380

381
        $_SESSION[$key] = $value;
1✔
382
    }
383

384
    /**
385
     * Magic method to get session variables by simply calling
386
     *  $foo = $session->foo;
387
     *
388
     * @return mixed
389
     */
390
    public function __get(string $key)
391
    {
392
        // Note: Keep this order the same, just in case somebody wants to
393
        // use 'session_id' as a session data key, for whatever reason
394
        if (isset($_SESSION[$key])) {
1✔
395
            return $_SESSION[$key];
1✔
396
        }
397

UNCOV
398
        if ($key === 'session_id') {
×
UNCOV
399
            return session_id();
×
400
        }
401

402
        return null;
×
403
    }
404

405
    /**
406
     * Magic method to check for session variables.
407
     *
408
     * Different from `has()` in that it will validate 'session_id' as well.
409
     * Mostly used by internal PHP functions, users should stick to `has()`.
410
     */
411
    public function __isset(string $key): bool
412
    {
413
        return isset($_SESSION[$key]) || $key === 'session_id';
2✔
414
    }
415

416
    public function setFlashdata($data, $value = null)
417
    {
418
        $this->set($data, $value);
11✔
419
        $this->markAsFlashdata(is_array($data) ? array_keys($data) : $data);
11✔
420
    }
421

422
    public function getFlashdata(?string $key = null)
423
    {
UNCOV
424
        $_SESSION['__ci_vars'] ??= [];
×
425

UNCOV
426
        if (isset($key)) {
×
UNCOV
427
            if (! isset($_SESSION['__ci_vars'][$key]) || is_int($_SESSION['__ci_vars'][$key])) {
×
428
                return null;
×
429
            }
430

431
            return $_SESSION[$key] ?? null;
×
432
        }
433

UNCOV
434
        $flashdata = [];
×
435

UNCOV
436
        foreach ($_SESSION['__ci_vars'] as $key => $value) {
×
UNCOV
437
            if (! is_int($value)) {
×
438
                $flashdata[$key] = $_SESSION[$key];
×
439
            }
440
        }
441

442
        return $flashdata;
×
443
    }
444

445
    public function keepFlashdata($key)
446
    {
447
        $this->markAsFlashdata($key);
1✔
448
    }
449

450
    public function markAsFlashdata($key): bool
451
    {
452
        $keys = is_array($key) ? $key : [$key];
12✔
453

454
        foreach ($keys as $sessionKey) {
12✔
455
            if (! isset($_SESSION[$sessionKey])) {
12✔
456
                return false;
1✔
457
            }
458
        }
459

460
        $_SESSION['__ci_vars'] ??= [];
11✔
461
        $_SESSION['__ci_vars'] = [...$_SESSION['__ci_vars'], ...array_fill_keys($keys, 'new')];
11✔
462

463
        return true;
11✔
464
    }
465

466
    public function unmarkFlashdata($key)
467
    {
468
        if (! isset($_SESSION['__ci_vars'])) {
1✔
UNCOV
469
            return;
×
470
        }
471

472
        if (! is_array($key)) {
1✔
473
            $key = [$key];
1✔
474
        }
475

476
        foreach ($key as $k) {
1✔
477
            if (isset($_SESSION['__ci_vars'][$k]) && ! is_int($_SESSION['__ci_vars'][$k])) {
1✔
478
                unset($_SESSION['__ci_vars'][$k]);
1✔
479
            }
480
        }
481

482
        if ($_SESSION['__ci_vars'] === []) {
1✔
483
            unset($_SESSION['__ci_vars']);
1✔
484
        }
485
    }
486

487
    public function getFlashKeys(): array
488
    {
489
        if (! isset($_SESSION['__ci_vars'])) {
2✔
490
            return [];
1✔
491
        }
492

493
        $keys = [];
1✔
494

495
        foreach (array_keys($_SESSION['__ci_vars']) as $key) {
1✔
496
            if (! is_int($_SESSION['__ci_vars'][$key])) {
1✔
497
                $keys[] = $key;
1✔
498
            }
499
        }
500

501
        return $keys;
1✔
502
    }
503

504
    public function setTempdata($data, $value = null, int $ttl = 300)
505
    {
506
        $this->set($data, $value);
10✔
507
        $this->markAsTempdata($data, $ttl);
10✔
508
    }
509

510
    public function getTempdata(?string $key = null)
511
    {
512
        $_SESSION['__ci_vars'] ??= [];
5✔
513

514
        if (isset($key)) {
5✔
515
            if (! isset($_SESSION['__ci_vars'][$key]) || ! is_int($_SESSION['__ci_vars'][$key])) {
1✔
UNCOV
516
                return null;
×
517
            }
518

519
            return $_SESSION[$key] ?? null;
1✔
520
        }
521

522
        $tempdata = [];
4✔
523

524
        foreach ($_SESSION['__ci_vars'] as $key => $value) {
4✔
525
            if (is_int($value)) {
3✔
526
                $tempdata[$key] = $_SESSION[$key];
3✔
527
            }
528
        }
529

530
        return $tempdata;
4✔
531
    }
532

533
    public function removeTempdata(string $key)
534
    {
535
        $this->unmarkTempdata($key);
1✔
536
        unset($_SESSION[$key]);
1✔
537
    }
538

539
    public function markAsTempdata($key, int $ttl = 300): bool
540
    {
541
        $time = Time::now()->getTimestamp();
11✔
542
        $keys = is_array($key) ? $key : [$key];
11✔
543

544
        if (array_is_list($keys)) {
11✔
545
            $keys = array_fill_keys($keys, $ttl);
4✔
546
        }
547

548
        $tempdata = [];
11✔
549

550
        foreach ($keys as $sessionKey => $timeToLive) {
11✔
551
            if (! array_key_exists($sessionKey, $_SESSION)) {
11✔
552
                return false;
1✔
553
            }
554

555
            if (is_int($timeToLive)) {
11✔
556
                $timeToLive += $time;
5✔
557
            } else {
558
                $timeToLive = $time + $ttl;
7✔
559
            }
560

561
            $tempdata[$sessionKey] = $timeToLive;
11✔
562
        }
563

564
        $_SESSION['__ci_vars'] ??= [];
10✔
565
        $_SESSION['__ci_vars'] = [...$_SESSION['__ci_vars'], ...$tempdata];
10✔
566

567
        return true;
10✔
568
    }
569

570
    public function unmarkTempdata($key)
571
    {
572
        if (! isset($_SESSION['__ci_vars'])) {
3✔
UNCOV
573
            return;
×
574
        }
575

576
        if (! is_array($key)) {
3✔
577
            $key = [$key];
2✔
578
        }
579

580
        foreach ($key as $k) {
3✔
581
            if (isset($_SESSION['__ci_vars'][$k]) && is_int($_SESSION['__ci_vars'][$k])) {
3✔
582
                unset($_SESSION['__ci_vars'][$k]);
3✔
583
            }
584
        }
585

586
        if ($_SESSION['__ci_vars'] === []) {
3✔
587
            unset($_SESSION['__ci_vars']);
1✔
588
        }
589
    }
590

591
    public function getTempKeys(): array
592
    {
593
        if (! isset($_SESSION['__ci_vars'])) {
2✔
594
            return [];
1✔
595
        }
596

597
        $keys = [];
1✔
598

599
        foreach (array_keys($_SESSION['__ci_vars']) as $key) {
1✔
600
            if (is_int($_SESSION['__ci_vars'][$key])) {
1✔
601
                $keys[] = $key;
1✔
602
            }
603
        }
604

605
        return $keys;
1✔
606
    }
607

608
    /**
609
     * Sets the driver as the session handler in PHP.
610
     * Extracted for easier testing.
611
     *
612
     * @return void
613
     */
614
    protected function setSaveHandler()
615
    {
616
        session_set_save_handler($this->driver, true);
48✔
617
    }
618

619
    /**
620
     * Starts the session.
621
     * Extracted for testing reasons.
622
     *
623
     * @return void
624
     */
625
    protected function startSession()
626
    {
627
        if (ENVIRONMENT === 'testing') {
48✔
628
            $_SESSION = [];
48✔
629

630
            return;
48✔
631
        }
632

633
        session_start(); // @codeCoverageIgnore
634
    }
635

636
    /**
637
     * Takes care of setting the cookie on the client side.
638
     *
639
     * @codeCoverageIgnore
640
     *
641
     * @return void
642
     */
643
    protected function setCookie()
644
    {
645
        $expiration   = $this->config->expiration === 0 ? 0 : Time::now()->getTimestamp() + $this->config->expiration;
646
        $this->cookie = $this->cookie->withValue(session_id())->withExpires($expiration);
647

648
        $response = service('response');
649
        $response->setCookie($this->cookie);
650
    }
651
}
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