diff --git a/app/Config/App.php b/app/Config/App.php index 3bba4a902197..3b083a48f4ed 100644 --- a/app/Config/App.php +++ b/app/Config/App.php @@ -334,6 +334,8 @@ class App extends BaseConfig * * The token name. * + * @deprecated Use `Config\Security` $tokenName property instead of using this property. + * * @var string */ public $CSRFTokenName = 'csrf_test_name'; @@ -345,6 +347,8 @@ class App extends BaseConfig * * The header name. * + * @deprecated Use `Config\Security` $headerName property instead of using this property. + * * @var string */ public $CSRFHeaderName = 'X-CSRF-TOKEN'; @@ -356,6 +360,8 @@ class App extends BaseConfig * * The cookie name. * + * @deprecated Use `Config\Security` $cookieName property instead of using this property. + * * @var string */ public $CSRFCookieName = 'csrf_cookie_name'; @@ -367,6 +373,8 @@ class App extends BaseConfig * * The number in seconds the token should expire. * + * @deprecated Use `Config\Security` $expire property instead of using this property. + * * @var integer */ public $CSRFExpire = 7200; @@ -378,6 +386,8 @@ class App extends BaseConfig * * Regenerate token on every submission? * + * @deprecated Use `Config\Security` $regenerate property instead of using this property. + * * @var boolean */ public $CSRFRegenerate = true; @@ -389,6 +399,8 @@ class App extends BaseConfig * * Redirect to previous page with error on failure? * + * @deprecated Use `Config\Security` $redirect property instead of using this property. + * * @var boolean */ public $CSRFRedirect = true; @@ -408,6 +420,8 @@ class App extends BaseConfig * * @see https://portswigger.net/web-security/csrf/samesite-cookies * + * @deprecated Use `Config\Security` $samesite property instead of using this property. + * * @var string */ public $CSRFSameSite = 'Lax'; diff --git a/app/Config/Security.php b/app/Config/Security.php new file mode 100644 index 000000000000..9dc91bb93d90 --- /dev/null +++ b/app/Config/Security.php @@ -0,0 +1,92 @@ +CSRFTokenName; + return Services::security()->getTokenName(); } } @@ -250,9 +248,7 @@ function csrf_token(): string */ function csrf_header(): string { - $config = config(App::class); - - return $config->CSRFHeaderName; + return Services::security()->getHeaderName(); } } @@ -267,9 +263,7 @@ function csrf_header(): string */ function csrf_hash(): string { - $security = Services::security(null, true); - - return $security->getCSRFHash(); + return Services::security()->getHash(); } } diff --git a/system/Config/Services.php b/system/Config/Services.php index b266eefff193..0b41dc0d3529 100644 --- a/system/Config/Services.php +++ b/system/Config/Services.php @@ -677,7 +677,7 @@ public static function security(App $config = null, bool $getShared = true) return static::getSharedInstance('security', $config); } - $config = $config ?? config('App'); + $config = $config ?? config('Security') ?? config('App'); return new Security($config); } @@ -859,6 +859,4 @@ public static function typography(bool $getShared = true) return new Typography(); } - - //-------------------------------------------------------------------- } diff --git a/system/Filters/CSRF.php b/system/Filters/CSRF.php index b756a5931547..4d767f7059e2 100644 --- a/system/Filters/CSRF.php +++ b/system/Filters/CSRF.php @@ -53,11 +53,11 @@ public function before(RequestInterface $request, $arguments = null) try { - $security->CSRFVerify($request); + $security->verify($request); } catch (SecurityException $e) { - if (config('App')->CSRFRedirect && ! $request->isAJAX()) + if ($security->shouldRedirect() && ! $request->isAJAX()) { return redirect()->back()->with('error', $e->getMessage()); } diff --git a/system/Language/en/Security.php b/system/Language/en/Security.php new file mode 100644 index 000000000000..3e1b9618a985 --- /dev/null +++ b/system/Language/en/Security.php @@ -0,0 +1,16 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +// Security language settings +return [ + 'disallowedAction' => 'The action you requested is not allowed.', + 'invalidSameSite' => 'The SameSite value must be None, Lax, Strict, or a blank string. Given: {0}', +]; diff --git a/system/Security/Exceptions/SecurityException.php b/system/Security/Exceptions/SecurityException.php index e4850920d3bf..c129ae534748 100644 --- a/system/Security/Exceptions/SecurityException.php +++ b/system/Security/Exceptions/SecurityException.php @@ -17,11 +17,11 @@ class SecurityException extends FrameworkException { public static function forDisallowedAction() { - return new static(lang('HTTP.disallowedAction'), 403); + return new static(lang('Security.disallowedAction'), 403); } - - public static function forInvalidSameSiteSetting(string $samesite) + + public static function forInvalidSameSite(string $samesite) { - return new static(lang('HTTP.invalidSameSiteSetting', [$samesite])); + return new static(lang('Security.invalidSameSite', [$samesite])); } } diff --git a/system/Security/Security.php b/system/Security/Security.php index 824bade66da2..d0ba69600fed 100644 --- a/system/Security/Security.php +++ b/system/Security/Security.php @@ -18,7 +18,10 @@ use Exception; /** - * HTTP security handler. + * Class Security + * + * Provides methods that help protect your site against + * Cross-Site Request Forgery attacks. */ class Security { @@ -29,162 +32,157 @@ class Security * * @var string|null */ - protected $CSRFHash; + protected $hash = null; /** - * CSRF Expire time - * - * Expiration time for Cross Site Request Forgery protection cookie. - * Defaults to two hours (in seconds). - * - * @var integer - */ - protected $CSRFExpire = 7200; - - /** - * CSRF Token name + * CSRF Token Name * * Token name for Cross Site Request Forgery protection cookie. * * @var string */ - protected $CSRFTokenName = 'CSRFToken'; + protected $tokenName = 'CSRFToken'; /** - * CSRF Header name + * CSRF Header Name * * Token name for Cross Site Request Forgery protection cookie. * * @var string */ - protected $CSRFHeaderName = 'CSRFToken'; + protected $headerName = 'CSRFToken'; /** - * CSRF Cookie name + * CSRF Cookie Name * * Cookie name for Cross Site Request Forgery protection cookie. * * @var string */ - protected $CSRFCookieName = 'CSRFToken'; + protected $cookieName = 'CSRFToken'; /** - * CSRF Regenerate + * CSRF Expires * - * If true, the CSRF Token will be regenerated on every request. - * If false, will stay the same for the life of the cookie. + * Expiration time for Cross Site Request Forgery protection cookie. * - * @var boolean - */ - protected $CSRFRegenerate = true; - - /** - * Typically will be a forward slash + * Defaults to two hours (in seconds). * - * @var string + * @var integer */ - protected $cookiePath = '/'; + protected $expires = 7200; /** - * Set to .your-domain.com for site-wide cookies + * CSRF Regenerate * - * @var string - */ - protected $cookieDomain = ''; - - /** - * Cookie will only be set if a secure HTTPS connection exists. + * Regenerate CSRF Token on every request. * * @var boolean */ - protected $cookieSecure = false; + protected $regenerate = true; /** - * SameSite setting of the CSRF cookie + * CSRF Redirect * - * @var string + * Redirect to previous page with error on failure. + * + * @var boolean */ - protected $CSRFSameSite = 'Lax'; + protected $redirect = true; /** - * List of sanitize filename strings + * CSRF SameSite + * + * Setting for CSRF SameSite cookie token. + * + * Allowed values are: None - Lax - Strict - ''. + * + * Defaults to `Lax` as recommended in this link: + * @see https://portswigger.net/web-security/csrf/samesite-cookies * - * @var array + * @var string */ - public $filenameBadChars = [ - '../', - '', - '<', - '>', - "'", - '"', - '&', - '$', - '#', - '{', - '}', - '[', - ']', - '=', - ';', - '?', - '%20', - '%22', - '%3c', // < - '%253c', // < - '%3e', // > - '%0e', // > - '%28', // ( - '%29', // ) - '%2528', // ( - '%26', // & - '%24', // $ - '%3f', // ? - '%3b', // ; - '%3d', // = - ]; + protected $samesite = 'Lax'; //-------------------------------------------------------------------- /** - * Security constructor. + * Constructor. * - * Stores our configuration and fires off the init() method to - * setup initial state. + * Stores our configuration and fires off the init() method to setup + * initial state. * * @param App $config * - * @throws Exception + * @throws SecurityException */ public function __construct($config) { - // Store our CSRF-related settings - $this->CSRFExpire = $config->CSRFExpire ?? $this->CSRFExpire; - $this->CSRFTokenName = $config->CSRFTokenName ?? $this->CSRFTokenName; - $this->CSRFHeaderName = $config->CSRFHeaderName ?? $this->CSRFHeaderName; - $this->CSRFCookieName = $config->CSRFCookieName ?? $this->CSRFCookieName; - $this->CSRFRegenerate = $config->CSRFRegenerate ?? $this->CSRFRegenerate; - $this->CSRFSameSite = $config->CSRFSameSite ?? $this->CSRFSameSite; - - if (isset($config->cookiePrefix)) + $security = config('Security'); + // Store CSRF-related configurations + $this->tokenName = $security->tokenName ?? $config->CSRFTokenName ?? $this->tokenName; + $this->headerName = $security->headerName ?? $config->CSRFHeaderName ?? $this->headerName; + $this->cookieName = $security->cookieName ?? $config->CSRFCookieName ?? $this->cookieName; + $this->expires = $security->expires ?? $config->CSRFExpire ?? $this->expires; + $this->regenerate = $security->regenerate ?? $config->CSRFRegenerate ?? $this->regenerate; + $this->samesite = $security->samesite ?? $config->CSRFSameSite ?? $this->samesite; + + if (! in_array(strtolower($this->samesite), ['none', 'lax', 'strict', ''], true)) { - $this->CSRFCookieName = $config->cookiePrefix . $this->CSRFCookieName; + throw SecurityException::forInvalidSameSite($this->samesite); } - if (! in_array(strtolower($this->CSRFSameSite), ['', 'none', 'lax', 'strict'], true)) + if (isset($config->cookiePrefix)) { - throw SecurityException::forInvalidSameSiteSetting($this->CSRFSameSite); + $this->cookieName = $config->cookiePrefix . $this->cookieName; } - // Store cookie-related settings - $this->cookiePath = $config->cookiePath ?? $this->cookiePath; - $this->cookieDomain = $config->cookieDomain ?? $this->cookieDomain; - $this->cookieSecure = $config->cookieSecure ?? $this->cookieSecure; + $this->generateHash(); + } - $this->CSRFSetHash(); + //-------------------------------------------------------------------- - unset($config); + /** + * CSRF Verify + * + * @param RequestInterface $request + * + * @return $this|false + * @throws SecurityException + * + * @deprecated Use `CodeIgniter\Security\Security::verify()` instead of using this method. + */ + public function CSRFVerify(RequestInterface $request) + { + return $this->verify($request); + } + + //-------------------------------------------------------------------- + + /** + * Returns the CSRF Hash. + * + * @return string|null + * + * @deprecated Use `CodeIgniter\Security\Security::getHash()` instead of using this method. + */ + public function getCSRFHash(): ?string + { + return $this->getHash(); + } + + //-------------------------------------------------------------------- + + /** + * Returns the CSRF Token Name. + * + * @return string + * + * @deprecated Use `CodeIgniter\Security\Security::getTokenName()` instead of using this method. + */ + public function getCSRFTokenName(): string + { + return $this->getTokenName(); } //-------------------------------------------------------------------- @@ -195,56 +193,68 @@ public function __construct($config) * @param RequestInterface $request * * @return $this|false - * @throws Exception + * @throws SecurityException */ - public function CSRFVerify(RequestInterface $request) + public function verify(RequestInterface $request) { - // If it's not a POST request we will set the CSRF cookie + // If it's not a POST request we will set the CSRF cookie. if (strtoupper($_SERVER['REQUEST_METHOD']) !== 'POST') { - return $this->CSRFSetCookie($request); + return $this->sendCookie($request); } - // Do the tokens exist in _POST, HEADER or optionally php:://input - json data - $CSRFTokenValue = $_POST[$this->CSRFTokenName] ?? - (! is_null($request->header($this->CSRFHeaderName)) && ! empty($request->header($this->CSRFHeaderName)->getValue()) ? - $request->header($this->CSRFHeaderName)->getValue() : - (! empty($request->getBody()) && ! empty($json = json_decode($request->getBody())) && json_last_error() === JSON_ERROR_NONE ? - ($json->{$this->CSRFTokenName} ?? null) : - null)); - - // Do the tokens exist in both the _POST/POSTed JSON and _COOKIE arrays? - if (! isset($CSRFTokenValue, $_COOKIE[$this->CSRFCookieName]) || $CSRFTokenValue !== $_COOKIE[$this->CSRFCookieName] - ) // Do the tokens match? + // Does the token exist in POST, HEADER or optionally php:://input - json data. + if ($request->hasHeader($this->headerName) && ! empty($request->getHeader($this->headerName)->getValue())) + { + $tokenName = $request->getHeader($this->headerName)->getValue(); + } + else + { + $json = json_decode($request->getBody()); + + if (! empty($request->getBody()) && ! empty($json) && json_last_error() === JSON_ERROR_NONE) + { + $tokenName = $json->{$this->tokenName} ?? null; + } + else + { + $tokenName = null; + } + } + + $token = $_POST[$this->tokenName] ?? $tokenName; + + // Does the tokens exist in both the POST/POSTed JSON and COOKIE arrays and match? + if (! isset($token, $_COOKIE[$this->cookieName]) || $token !== $_COOKIE[$this->cookieName]) { throw SecurityException::forDisallowedAction(); } - // We kill this since we're done and we don't want to pollute the _POST array - if (isset($_POST[$this->CSRFTokenName])) + if (isset($_POST[$this->tokenName])) { - unset($_POST[$this->CSRFTokenName]); + // We kill this since we're done and we don't want to pollute the POST array. + unset($_POST[$this->tokenName]); $request->setGlobal('post', $_POST); } - // We kill this since we're done and we don't want to pollute the JSON data - elseif (isset($json->{$this->CSRFTokenName})) + elseif (isset($json->{$this->tokenName})) { - unset($json->{$this->CSRFTokenName}); + // We kill this since we're done and we don't want to pollute the JSON data. + unset($json->{$this->tokenName}); $request->setBody(json_encode($json)); } // Regenerate on every submission? - if ($this->CSRFRegenerate) + if ($this->regenerate) { - // Nothing should last forever - $this->CSRFHash = null; - unset($_COOKIE[$this->CSRFCookieName]); + // Nothing should last forever. + $this->hash = null; + unset($_COOKIE[$this->cookieName]); } - $this->CSRFSetHash(); - $this->CSRFSetCookie($request); + $this->generateHash(); + $this->sendCookie($request); - log_message('info', 'CSRF token verified'); + log_message('info', 'CSRF token verified.'); return $this; } @@ -252,122 +262,73 @@ public function CSRFVerify(RequestInterface $request) //-------------------------------------------------------------------- /** - * CSRF Set Cookie + * Returns the CSRF Hash. * - * @codeCoverageIgnore - * - * @param RequestInterface|IncomingRequest $request - * - * @return Security|false + * @return string|null */ - public function CSRFSetCookie(RequestInterface $request) + public function getHash(): ?string { - $expire = $this->CSRFExpire === 0 ? $this->CSRFExpire : time() + $this->CSRFExpire; - $secureCookie = (bool) $this->cookieSecure; - - if ($secureCookie && ! $request->isSecure()) - { - return false; - } - - if (PHP_VERSION_ID < 70300) - { - // In PHP < 7.3.0, there is a "hacky" way to set the samesite parameter - $samesite = ''; - if ($this->CSRFSameSite !== '') - { - $samesite = '; samesite=' . $this->CSRFSameSite; - } - - setcookie( - $this->CSRFCookieName, - $this->CSRFHash, - $expire, - $this->cookiePath . $samesite, - $this->cookieDomain, - $secureCookie, - true // Enforce HTTP only cookie for security - ); - } - else - { - // PHP 7.3 adds another function signature allowing setting of samesite - $params = [ - 'expires' => $expire, - 'path' => $this->cookiePath, - 'domain' => $this->cookieDomain, - 'secure' => $secureCookie, - 'httponly' => true, // Enforce HTTP only cookie for security - ]; - - if ($this->CSRFSameSite !== '') - { - $params['samesite'] = $this->CSRFSameSite; - } - - setcookie( - $this->CSRFCookieName, - $this->CSRFHash, - $params - ); - } + return $this->hash; + } - log_message('info', 'CSRF cookie sent'); + //-------------------------------------------------------------------- - return $this; + /** + * Returns the CSRF Token Name. + * + * @return string + */ + public function getTokenName(): string + { + return $this->tokenName; } //-------------------------------------------------------------------- /** - * Returns the current CSRF Hash. + * Returns the CSRF Header Name. * - * @return string|null + * @return string */ - public function getCSRFHash(): ?string + public function getHeaderName(): string { - return $this->CSRFHash; + return $this->headerName; } //-------------------------------------------------------------------- /** - * Returns the CSRF Token Name. + * Returns the CSRF Cookie Name. * * @return string */ - public function getCSRFTokenName(): string + public function getCookieName(): string { - return $this->CSRFTokenName; + return $this->cookieName; } //-------------------------------------------------------------------- /** - * Sets the CSRF Hash and cookie. + * Check if CSRF cookie is expired. * - * @return string - * @throws Exception + * @return boolean */ - protected function CSRFSetHash(): string + public function isExpired(): bool { - if ($this->CSRFHash === null) - { - // If the cookie exists we will use its value. - // We don't necessarily want to regenerate it with - // each page load since a page could contain embedded - // sub-pages causing this feature to fail - if (isset($_COOKIE[$this->CSRFCookieName]) && is_string($_COOKIE[$this->CSRFCookieName]) && preg_match('#^[0-9a-f]{32}$#iS', $_COOKIE[$this->CSRFCookieName]) === 1 - ) - { - return $this->CSRFHash = $_COOKIE[$this->CSRFCookieName]; - } + return $this->expires === 0; + } - $rand = random_bytes(16); - $this->CSRFHash = bin2hex($rand); - } + //-------------------------------------------------------------------- - return $this->CSRFHash; + /** + * Check if request should be redirect on failure. + * + * @return boolean + */ + public function shouldRedirect(): bool + { + return $this->redirect; } //-------------------------------------------------------------------- @@ -390,7 +351,11 @@ protected function CSRFSetHash(): string */ public function sanitizeFilename(string $str, bool $relativePath = false): string { - $bad = $this->filenameBadChars; + // List of sanitize filename strings + $bad = [ + '../', '', '<', '>', "'", '"', '&', '$', '#', '{', '}', '[', ']', '=', ';', '?', + '%20', '%22', '%3c', '%253c', '%3e', '%0e', '%28', '%29', '%2528', '%26', '%24', '%3f', '%3b', '%3d', + ]; if (! $relativePath) { @@ -411,4 +376,92 @@ public function sanitizeFilename(string $str, bool $relativePath = false): strin } //-------------------------------------------------------------------- + + /** + * Generates the CSRF Hash. + * + * @return string + */ + protected function generateHash(): string + { + if (is_null($this->hash)) + { + // If the cookie exists we will use its value. + // We don't necessarily want to regenerate it with + // each page load since a page could contain embedded + // sub-pages causing this feature to fail + if ( + isset($_COOKIE[$this->cookieName]) + && is_string($_COOKIE[$this->cookieName]) + && preg_match('#^[0-9a-f]{32}$#iS', $_COOKIE[$this->cookieName]) === 1 + ) + { + return $this->hash = $_COOKIE[$this->cookieName]; + } + + $this->hash = bin2hex(random_bytes(16)); + } + + return $this->hash; + } + + //-------------------------------------------------------------------- + + /** + * CSRF Send Cookie + * + * @param RequestInterface $request + * + * @return Security|false + * @codeCoverageIgnore + */ + protected function sendCookie(RequestInterface $request) + { + $config = new App(); + + $expires = $this->isExpired() ? $this->expires : time() + $this->expires; + $path = $config->cookiePath ?? '/'; + $domain = $config->cookieDomain ?? ''; + $secure = $config->cookieSecure ?? false; + + if ($secure && ! $request->isSecure()) + { + return false; + } + + if (PHP_VERSION_ID < 70300) + { + // In PHP < 7.3.0, there is a "hacky" way to set the samesite parameter + $samesite = ''; + + if (! empty($this->samesite)) + { + $samesite = '; samesite=' . $this->samesite; + } + + setcookie($this->cookieName, $this->hash, $expires, $path . $samesite, $domain, $secure, true); + } + else + { + // PHP 7.3 adds another function signature allowing setting of samesite + $params = [ + 'expires' => $expires, + 'path' => $path, + 'domain' => $domain, + 'secure' => $secure, + 'httponly' => true, // Enforce HTTP only cookie for security + ]; + + if (! empty($this->samesite)) + { + $params['samesite'] = $this->samesite; + } + + setcookie($this->cookieName, $this->hash, $params); + } + + log_message('info', 'CSRF cookie sent.'); + + return $this; + } } diff --git a/system/Test/Mock/MockSecurity.php b/system/Test/Mock/MockSecurity.php index 379f168a7712..3570e5c380df 100644 --- a/system/Test/Mock/MockSecurity.php +++ b/system/Test/Mock/MockSecurity.php @@ -16,13 +16,10 @@ class MockSecurity extends Security { - public function CSRFSetCookie(RequestInterface $request) + public function sendCookie(RequestInterface $request) { - $_COOKIE['csrf_cookie_name'] = $this->CSRFHash; + $_COOKIE['csrf_cookie_name'] = $this->hash; return $this; } - - //-------------------------------------------------------------------- - } diff --git a/system/Test/Mock/MockSecurityConfig.php b/system/Test/Mock/MockSecurityConfig.php new file mode 100644 index 000000000000..a0786a97afd4 --- /dev/null +++ b/system/Test/Mock/MockSecurityConfig.php @@ -0,0 +1,17 @@ +getCSRFHash(); + $hash = $security->getHash(); $this->assertEquals(32, strlen($hash)); - $this->assertEquals('csrf_test_name', $security->getCSRFTokenName()); + $this->assertEquals('csrf_test_name', $security->getTokenName()); } //-------------------------------------------------------------------- public function testHashIsReadFromCookie() { - $_COOKIE = [ - 'csrf_cookie_name' => '8b9218a55906f9dcc1dc263dce7f005a', - ]; + $_COOKIE['csrf_cookie_name'] = '8b9218a55906f9dcc1dc263dce7f005a'; - $security = new Security(new MockAppConfig()); + $security = new Security(new MockSecurityConfig()); - $this->assertEquals('8b9218a55906f9dcc1dc263dce7f005a', $security->getCSRFHash()); + $this->assertEquals('8b9218a55906f9dcc1dc263dce7f005a', $security->getHash()); } //-------------------------------------------------------------------- public function testCSRFVerifySetsCookieWhenNotPOST() { - $security = new MockSecurity(new MockAppConfig()); + $security = new MockSecurity(new MockSecurityConfig()); $_SERVER['REQUEST_METHOD'] = 'GET'; - $security->CSRFVerify(new Request(new MockAppConfig())); + $security->verify(new Request(new MockAppConfig())); - $this->assertEquals($_COOKIE['csrf_cookie_name'], $security->getCSRFHash()); + $this->assertEquals($_COOKIE['csrf_cookie_name'], $security->getHash()); } //-------------------------------------------------------------------- public function testCSRFVerifyPostThrowsExceptionOnNoMatch() { - $security = new MockSecurity(new MockAppConfig()); + $security = new MockSecurity(new MockSecurityConfig()); $request = new IncomingRequest(new MockAppConfig(), new URI('http://badurl.com'), null, new UserAgent()); - $_SERVER['REQUEST_METHOD'] = 'POST'; - $_POST['csrf_test_name'] = '8b9218a55906f9dcc1dc263dce7f005a'; - $_COOKIE = [ - 'csrf_cookie_name' => '8b9218a55906f9dcc1dc263dce7f005b', - ]; + $_SERVER['REQUEST_METHOD'] = 'POST'; + $_POST['csrf_test_name'] = '8b9218a55906f9dcc1dc263dce7f005a'; + $_COOKIE['csrf_cookie_name'] = '8b9218a55906f9dcc1dc263dce7f005b'; - $this->expectException(SecurityException::class); - $security->CSRFVerify($request); + $this->expectException('CodeIgniter\Security\Exceptions\SecurityException'); + $security->verify($request); } //-------------------------------------------------------------------- public function testCSRFVerifyPostReturnsSelfOnMatch() { - $security = new MockSecurity(new MockAppConfig()); + $security = new MockSecurity(new MockSecurityConfig()); $request = new IncomingRequest(new MockAppConfig(), new URI('http://badurl.com'), null, new UserAgent()); - $_SERVER['REQUEST_METHOD'] = 'POST'; - $_POST['foo'] = 'bar'; - $_POST['csrf_test_name'] = '8b9218a55906f9dcc1dc263dce7f005a'; - $_COOKIE = [ - 'csrf_cookie_name' => '8b9218a55906f9dcc1dc263dce7f005a', - ]; + $_SERVER['REQUEST_METHOD'] = 'POST'; + $_POST['foo'] = 'bar'; + $_POST['csrf_test_name'] = '8b9218a55906f9dcc1dc263dce7f005a'; + $_COOKIE['csrf_cookie_name'] = '8b9218a55906f9dcc1dc263dce7f005a'; - $this->assertInstanceOf('CodeIgniter\Security\Security', $security->CSRFVerify($request)); - $this->assertLogged('info', 'CSRF token verified'); + $this->assertInstanceOf('CodeIgniter\Security\Security', $security->verify($request)); + $this->assertLogged('info', 'CSRF token verified.'); $this->assertTrue(count($_POST) === 1); } @@ -101,37 +97,33 @@ public function testCSRFVerifyPostReturnsSelfOnMatch() public function testCSRFVerifyHeaderThrowsExceptionOnNoMatch() { - $security = new MockSecurity(new MockAppConfig()); + $security = new MockSecurity(new MockSecurityConfig()); $request = new IncomingRequest(new MockAppConfig(), new URI('http://badurl.com'), null, new UserAgent()); $request->setHeader('X-CSRF-TOKEN', '8b9218a55906f9dcc1dc263dce7f005a'); - $_SERVER['REQUEST_METHOD'] = 'POST'; - $_COOKIE = [ - 'csrf_cookie_name' => '8b9218a55906f9dcc1dc263dce7f005b', - ]; + $_SERVER['REQUEST_METHOD'] = 'POST'; + $_COOKIE['csrf_cookie_name'] = '8b9218a55906f9dcc1dc263dce7f005b'; - $this->expectException(SecurityException::class); - $security->CSRFVerify($request); + $this->expectException('CodeIgniter\Security\Exceptions\SecurityException'); + $security->verify($request); } //-------------------------------------------------------------------- public function testCSRFVerifyHeaderReturnsSelfOnMatch() { - $security = new MockSecurity(new MockAppConfig()); + $security = new MockSecurity(new MockSecurityConfig()); $request = new IncomingRequest(new MockAppConfig(), new URI('http://badurl.com'), null, new UserAgent()); $request->setHeader('X-CSRF-TOKEN', '8b9218a55906f9dcc1dc263dce7f005a'); - $_SERVER['REQUEST_METHOD'] = 'POST'; - $_POST['foo'] = 'bar'; - $_COOKIE = [ - 'csrf_cookie_name' => '8b9218a55906f9dcc1dc263dce7f005a', - ]; + $_SERVER['REQUEST_METHOD'] = 'POST'; + $_POST['foo'] = 'bar'; + $_COOKIE['csrf_cookie_name'] = '8b9218a55906f9dcc1dc263dce7f005a'; - $this->assertInstanceOf('CodeIgniter\Security\Security', $security->CSRFVerify($request)); - $this->assertLogged('info', 'CSRF token verified'); + $this->assertInstanceOf('CodeIgniter\Security\Security', $security->verify($request)); + $this->assertLogged('info', 'CSRF token verified.'); $this->assertTrue(count($_POST) === 1); } @@ -140,36 +132,32 @@ public function testCSRFVerifyHeaderReturnsSelfOnMatch() public function testCSRFVerifyJsonThrowsExceptionOnNoMatch() { - $security = new MockSecurity(new MockAppConfig()); + $security = new MockSecurity(new MockSecurityConfig()); $request = new IncomingRequest(new MockAppConfig(), new URI('http://badurl.com'), null, new UserAgent()); $request->setBody('{"csrf_test_name":"8b9218a55906f9dcc1dc263dce7f005a"}'); - $_SERVER['REQUEST_METHOD'] = 'POST'; - $_COOKIE = [ - 'csrf_cookie_name' => '8b9218a55906f9dcc1dc263dce7f005b', - ]; + $_SERVER['REQUEST_METHOD'] = 'POST'; + $_COOKIE['csrf_cookie_name'] = '8b9218a55906f9dcc1dc263dce7f005b'; - $this->expectException(SecurityException::class); - $security->CSRFVerify($request); + $this->expectException('CodeIgniter\Security\Exceptions\SecurityException'); + $security->verify($request); } //-------------------------------------------------------------------- public function testCSRFVerifyJsonReturnsSelfOnMatch() { - $security = new MockSecurity(new MockAppConfig()); + $security = new MockSecurity(new MockSecurityConfig()); $request = new IncomingRequest(new MockAppConfig(), new URI('http://badurl.com'), null, new UserAgent()); $request->setBody('{"csrf_test_name":"8b9218a55906f9dcc1dc263dce7f005a","foo":"bar"}'); - $_SERVER['REQUEST_METHOD'] = 'POST'; - $_COOKIE = [ - 'csrf_cookie_name' => '8b9218a55906f9dcc1dc263dce7f005a', - ]; + $_SERVER['REQUEST_METHOD'] = 'POST'; + $_COOKIE['csrf_cookie_name'] = '8b9218a55906f9dcc1dc263dce7f005a'; - $this->assertInstanceOf('CodeIgniter\Security\Security', $security->CSRFVerify($request)); - $this->assertLogged('info', 'CSRF token verified'); + $this->assertInstanceOf('CodeIgniter\Security\Security', $security->verify($request)); + $this->assertLogged('info', 'CSRF token verified.'); $this->assertTrue($request->getBody() === '{"foo":"bar"}'); } @@ -178,13 +166,10 @@ public function testCSRFVerifyJsonReturnsSelfOnMatch() public function testSanitizeFilename() { - $security = new MockSecurity(new MockAppConfig()); + $security = new MockSecurity(new MockSecurityConfig()); $filename = './'; $this->assertEquals('foo', $security->sanitizeFilename($filename)); } - - //-------------------------------------------------------------------- - } diff --git a/tests/system/Session/SessionTest.php b/tests/system/Session/SessionTest.php index b1132ff8f7f9..ceb909660aab 100644 --- a/tests/system/Session/SessionTest.php +++ b/tests/system/Session/SessionTest.php @@ -1,8 +1,11 @@ -expectException(SessionException::class); - $this->expectExceptionMessage(lang('HTTP.invalidSameSiteSetting', ['Invalid'])); + $this->expectExceptionMessage(lang('Session.invalidSameSiteSetting', ['Invalid'])); $session = $this->getInstance(['cookieSameSite' => 'Invalid']); $session->start(); diff --git a/user_guide_src/source/changelogs/v4.0.5.rst b/user_guide_src/source/changelogs/v4.0.5.rst index cbc7a8aae856..75476079a378 100644 --- a/user_guide_src/source/changelogs/v4.0.5.rst +++ b/user_guide_src/source/changelogs/v4.0.5.rst @@ -23,6 +23,16 @@ Deprecations: - Deprecated ``BaseCommand::getPad`` in favor of ``BaseCommand::setPad``. - Deprecated ``CodeIgniter\Controller::loadHelpers`` in favor of ``helper`` function. - Deprecated ``Config\Format::getFormatter`` in favor of ``CodeIgniter\Format\Format::getFormatter`` +- Deprecated ``CodeIgniter\Security\Security::CSRFVerify`` in favor of ``CodeIgniter\Security\Security::verify`` +- Deprecated ``CodeIgniter\Security\Security::getCSRFHash`` in favor of ``CodeIgniter\Security\Security::getHash`` +- Deprecated ``CodeIgniter\Security\Security::getCSRTokenName`` in favor of ``CodeIgniter\Security\Security::getCSRTokenName`` +- Deprecated ``Config\App::$CSRFTokenName`` in favor of ``Config\Security::$tokenName`` +- Deprecated ``Config\App::$CSRFHeaderName`` in favor of ``Config\Security::$headerName`` +- Deprecated ``Config\App::$CSRFCookieName`` in favor of ``Config\Security::$cookieName`` +- Deprecated ``Config\App::$CSRFExpire`` in favor of ``Config\Security::$expire`` +- Deprecated ``Config\App::$CSRFRegenerate`` in favor of ``Config\Security::$regenerate`` +- Deprecated ``Config\App::$CSRFRedirect`` in favor of ``Config\Security::$redirect`` +- Deprecated ``Config\App::$CSRFSameSite`` in favor of ``Config\Security::$samesite`` - Deprecated ``migrate:create`` command in favor of ``make:migration`` command. - Deprecated ``CodeIgniter\Database\ModelFactory`` in favor of ``CodeIgniter\Config\Factories::models()`` - Deprecated ``CodeIgniter\Config\Config`` in favor of ``CodeIgniter\Config\Factories::config()`` diff --git a/user_guide_src/source/libraries/security.rst b/user_guide_src/source/libraries/security.rst index 9e6a89d51b7f..503268c0878c 100644 --- a/user_guide_src/source/libraries/security.rst +++ b/user_guide_src/source/libraries/security.rst @@ -1,6 +1,6 @@ -############## -Security Class -############## +######## +Security +######## The Security Class contains methods that help protect your site against Cross-Site Request Forgery attacks. @@ -86,17 +86,17 @@ kept the same throughout the life of the CSRF cookie. The default regeneration of tokens provides stricter security, but may result in usability concerns as other tokens become invalid (back/forward navigation, multiple tabs/windows, asynchronous actions, etc). You -may alter this behavior by editing the following config parameter -:: +may alter this behavior by editing the following config parameter value in +**app/Config/Security.php**:: - public $CSRFRegenerate = true; + public $regenerate = true; When a request fails the CSRF validation check, it will redirect to the previous page by default, setting an ``error`` flash message that you can display to the end user. This provides a nicer experience -than simply crashing. This can be turned off by editing the ``$CSRFRedirect`` value in -**app/Config/App.php**:: +than simply crashing. This can be turned off by editing the following config parameter value in +**app/Config/Security.php**:: - public $CSRFRedirect = false; + public $redirect = false; Even when the redirect value is **true**, AJAX calls will not redirect, but will throw an error.