Skip to content

Commit 76ad2ad

Browse files
authored
Merge pull request #5429 from kenjis/fix-security-sends-cookies
fix: Security class sends cookies immediately
2 parents 23d9047 + 49cd879 commit 76ad2ad

File tree

11 files changed

+222
-89
lines changed

11 files changed

+222
-89
lines changed

depfile.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -172,6 +172,7 @@ ruleset:
172172
HTTP:
173173
- Cookie
174174
- Files
175+
- Security
175176
- URI
176177
Images:
177178
- Files

system/Cookie/CookieStore.php

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -158,6 +158,8 @@ public function remove(string $name, string $prefix = '')
158158

159159
/**
160160
* Dispatches all cookies in store.
161+
*
162+
* @deprecated Response should dispatch cookies.
161163
*/
162164
public function dispatch(): void
163165
{
@@ -232,6 +234,8 @@ protected function validateCookies(array $cookies): void
232234
* Extracted call to `setrawcookie()` in order to run unit tests on it.
233235
*
234236
* @codeCoverageIgnore
237+
*
238+
* @deprecated
235239
*/
236240
protected function setRawCookie(string $name, string $value, array $options): void
237241
{
@@ -242,6 +246,8 @@ protected function setRawCookie(string $name, string $value, array $options): vo
242246
* Extracted call to `setcookie()` in order to run unit tests on it.
243247
*
244248
* @codeCoverageIgnore
249+
*
250+
* @deprecated
245251
*/
246252
protected function setCookie(string $name, string $value, array $options): void
247253
{

system/HTTP/ResponseTrait.php

Lines changed: 63 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -16,13 +16,14 @@
1616
use CodeIgniter\Cookie\Exceptions\CookieException;
1717
use CodeIgniter\HTTP\Exceptions\HTTPException;
1818
use CodeIgniter\Pager\PagerInterface;
19+
use CodeIgniter\Security\Exceptions\SecurityException;
1920
use Config\Services;
2021
use DateTime;
2122
use DateTimeZone;
2223
use InvalidArgumentException;
2324

2425
/**
25-
* Request Trait
26+
* Response Trait
2627
*
2728
* Additional methods to make a PSR-7 Response class
2829
* compliant with the framework's own ResponseInterface.
@@ -446,7 +447,7 @@ public function send()
446447
}
447448

448449
/**
449-
* Sends the headers of this HTTP request to the browser.
450+
* Sends the headers of this HTTP response to the browser.
450451
*
451452
* @return Response
452453
*/
@@ -535,15 +536,15 @@ public function redirect(string $uri, string $method = 'auto', ?int $code = null
535536
* Accepts an arbitrary number of binds (up to 7) or an associative
536537
* array in the first parameter containing all the values.
537538
*
538-
* @param array|string $name Cookie name or array containing binds
539-
* @param string $value Cookie value
540-
* @param string $expire Cookie expiration time in seconds
541-
* @param string $domain Cookie domain (e.g.: '.yourdomain.com')
542-
* @param string $path Cookie path (default: '/')
543-
* @param string $prefix Cookie name prefix
544-
* @param bool $secure Whether to only transfer cookies via SSL
545-
* @param bool $httponly Whether only make the cookie accessible via HTTP (no javascript)
546-
* @param string|null $samesite
539+
* @param array|Cookie|string $name Cookie name / array containing binds / Cookie object
540+
* @param string $value Cookie value
541+
* @param string $expire Cookie expiration time in seconds
542+
* @param string $domain Cookie domain (e.g.: '.yourdomain.com')
543+
* @param string $path Cookie path (default: '/')
544+
* @param string $prefix Cookie name prefix
545+
* @param bool $secure Whether to only transfer cookies via SSL
546+
* @param bool $httponly Whether only make the cookie accessible via HTTP (no javascript)
547+
* @param string|null $samesite
547548
*
548549
* @return $this
549550
*/
@@ -558,6 +559,12 @@ public function setCookie(
558559
$httponly = false,
559560
$samesite = null
560561
) {
562+
if ($name instanceof Cookie) {
563+
$this->cookieStore = $this->cookieStore->put($name);
564+
565+
return $this;
566+
}
567+
561568
if (is_array($name)) {
562569
// always leave 'name' in last place, as the loop will break otherwise, due to $$item
563570
foreach (['samesite', 'value', 'expire', 'domain', 'path', 'prefix', 'secure', 'httponly', 'name'] as $item) {
@@ -689,7 +696,51 @@ protected function sendCookies()
689696
return;
690697
}
691698

692-
$this->cookieStore->dispatch();
699+
$this->dispatchCookies();
700+
}
701+
702+
private function dispatchCookies(): void
703+
{
704+
/** @var IncomingRequest $request */
705+
$request = Services::request();
706+
707+
foreach ($this->cookieStore->display() as $cookie) {
708+
if ($cookie->isSecure() && ! $request->isSecure()) {
709+
throw SecurityException::forDisallowedAction();
710+
}
711+
712+
$name = $cookie->getPrefixedName();
713+
$value = $cookie->getValue();
714+
$options = $cookie->getOptions();
715+
716+
if ($cookie->isRaw()) {
717+
$this->doSetRawCookie($name, $value, $options);
718+
} else {
719+
$this->doSetCookie($name, $value, $options);
720+
}
721+
}
722+
723+
$this->cookieStore->clear();
724+
}
725+
726+
/**
727+
* Extracted call to `setrawcookie()` in order to run unit tests on it.
728+
*
729+
* @codeCoverageIgnore
730+
*/
731+
private function doSetRawCookie(string $name, string $value, array $options): void
732+
{
733+
setrawcookie($name, $value, $options);
734+
}
735+
736+
/**
737+
* Extracted call to `setcookie()` in order to run unit tests on it.
738+
*
739+
* @codeCoverageIgnore
740+
*/
741+
private function doSetCookie(string $name, string $value, array $options): void
742+
{
743+
setcookie($name, $value, $options);
693744
}
694745

695746
/**

system/Security/Security.php

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313

1414
use CodeIgniter\Cookie\Cookie;
1515
use CodeIgniter\HTTP\RequestInterface;
16+
use CodeIgniter\HTTP\Response;
1617
use CodeIgniter\Security\Exceptions\SecurityException;
1718
use CodeIgniter\Session\Session;
1819
use Config\App;
@@ -528,13 +529,18 @@ private function saveHashInCookie(): void
528529
'expires' => $this->expires === 0 ? 0 : time() + $this->expires,
529530
]
530531
);
531-
$this->sendCookie($this->request);
532+
533+
/** @var Response $response */
534+
$response = Services::response();
535+
$response->setCookie($this->cookie);
532536
}
533537

534538
/**
535539
* CSRF Send Cookie
536540
*
537541
* @return false|Security
542+
*
543+
* @deprecated Set cookies to Response object instead.
538544
*/
539545
protected function sendCookie(RequestInterface $request)
540546
{
@@ -553,6 +559,8 @@ protected function sendCookie(RequestInterface $request)
553559
* Extracted for this to be unit tested.
554560
*
555561
* @codeCoverageIgnore
562+
*
563+
* @deprecated Set cookies to Response object instead.
556564
*/
557565
protected function doSendCookie(): void
558566
{

tests/system/CommonFunctionsTest.php

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -41,10 +41,10 @@ final class CommonFunctionsTest extends CIUnitTestCase
4141
{
4242
protected function setUp(): void
4343
{
44-
parent::setUp();
45-
$renderer = Services::renderer();
46-
$renderer->resetData();
4744
unset($_ENV['foo'], $_SERVER['foo']);
45+
Services::reset();
46+
47+
parent::setUp();
4848
}
4949

5050
public function testStringifyAttributes()

tests/system/HTTP/ResponseCookieTest.php

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,17 @@ public function testCookiesAll()
6565
$this->assertTrue($response->hasCookie('bee'));
6666
}
6767

68+
public function testSetCookieObject()
69+
{
70+
$cookie = new Cookie('foo', 'bar');
71+
$response = new Response(new App());
72+
73+
$response->setCookie($cookie);
74+
75+
$this->assertCount(1, $response->getCookies());
76+
$this->assertTrue($response->hasCookie('foo'));
77+
}
78+
6879
public function testCookieGet()
6980
{
7081
$response = new Response(new App());

tests/system/HTTP/ResponseSendTest.php

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,10 @@
1111

1212
namespace CodeIgniter\HTTP;
1313

14+
use CodeIgniter\Security\Exceptions\SecurityException;
1415
use CodeIgniter\Test\CIUnitTestCase;
1516
use Config\App;
17+
use Config\Services;
1618

1719
/**
1820
* This test suite has been created separately from
@@ -135,4 +137,38 @@ public function testRedirectResponseCookies()
135137
$this->assertHeaderEmitted('Set-Cookie: foo=bar;');
136138
$this->assertHeaderEmitted('Set-Cookie: login_time');
137139
}
140+
141+
/**
142+
* Make sure secure cookies are not sent with HTTP request
143+
*
144+
* @ runInSeparateProcess
145+
* @ preserveGlobalState disabled
146+
*/
147+
public function testDoNotSendUnSecureCookie(): void
148+
{
149+
$this->expectException(SecurityException::class);
150+
$this->expectExceptionMessage('The action you requested is not allowed');
151+
152+
$request = $this->createMock(IncomingRequest::class);
153+
$request->method('isSecure')->willReturn(false);
154+
Services::injectMock('request', $request);
155+
156+
$response = new Response(new App());
157+
$response->pretend(false);
158+
$body = 'Hello';
159+
$response->setBody($body);
160+
161+
$response->setCookie(
162+
'foo',
163+
'bar',
164+
'',
165+
'',
166+
'/',
167+
'',
168+
true
169+
);
170+
171+
// send it
172+
$response->send();
173+
}
138174
}

tests/system/HTTP/ResponseTest.php

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,14 +30,18 @@ final class ResponseTest extends CIUnitTestCase
3030

3131
protected function setUp(): void
3232
{
33-
parent::setUp();
3433
$this->server = $_SERVER;
34+
35+
Services::reset();
36+
37+
parent::setUp();
3538
}
3639

3740
protected function tearDown(): void
3841
{
39-
$_SERVER = $this->server;
4042
Factories::reset('config');
43+
44+
$_SERVER = $this->server;
4145
}
4246

4347
public function testCanSetStatusCode()

0 commit comments

Comments
 (0)