464 lines
9.2 KiB
PHP
464 lines
9.2 KiB
PHP
<?php
|
|
|
|
/**
|
|
* This file is part of the CodeIgniter 4 framework.
|
|
*
|
|
* (c) CodeIgniter Foundation <admin@codeigniter.com>
|
|
*
|
|
* For the full copyright and license information, please view the LICENSE
|
|
* file that was distributed with this source code.
|
|
*/
|
|
|
|
namespace CodeIgniter\Security;
|
|
|
|
use CodeIgniter\Cookie\Cookie;
|
|
use CodeIgniter\HTTP\RequestInterface;
|
|
use CodeIgniter\Security\Exceptions\SecurityException;
|
|
use Config\App;
|
|
use Config\Cookie as CookieConfig;
|
|
use Config\Security as SecurityConfig;
|
|
|
|
/**
|
|
* Class Security
|
|
*
|
|
* Provides methods that help protect your site against
|
|
* Cross-Site Request Forgery attacks.
|
|
*/
|
|
class Security implements SecurityInterface
|
|
{
|
|
/**
|
|
* CSRF Hash
|
|
*
|
|
* Random hash for Cross Site Request Forgery protection cookie
|
|
*
|
|
* @var string|null
|
|
*/
|
|
protected $hash = null;
|
|
|
|
/**
|
|
* CSRF Token Name
|
|
*
|
|
* Token name for Cross Site Request Forgery protection cookie.
|
|
*
|
|
* @var string
|
|
*/
|
|
protected $tokenName = 'csrf_token_name';
|
|
|
|
/**
|
|
* CSRF Header Name
|
|
*
|
|
* Token name for Cross Site Request Forgery protection cookie.
|
|
*
|
|
* @var string
|
|
*/
|
|
protected $headerName = 'X-CSRF-TOKEN';
|
|
|
|
/**
|
|
* The CSRF Cookie instance.
|
|
*
|
|
* @var Cookie
|
|
*/
|
|
protected $cookie;
|
|
|
|
/**
|
|
* CSRF Cookie Name
|
|
*
|
|
* Cookie name for Cross Site Request Forgery protection cookie.
|
|
*
|
|
* @var string
|
|
*/
|
|
protected $cookieName = 'csrf_cookie_name';
|
|
|
|
/**
|
|
* CSRF Expires
|
|
*
|
|
* Expiration time for Cross Site Request Forgery protection cookie.
|
|
*
|
|
* Defaults to two hours (in seconds).
|
|
*
|
|
* @var integer
|
|
*
|
|
* @deprecated
|
|
*/
|
|
protected $expires = 7200;
|
|
|
|
/**
|
|
* CSRF Regenerate
|
|
*
|
|
* Regenerate CSRF Token on every request.
|
|
*
|
|
* @var boolean
|
|
*/
|
|
protected $regenerate = true;
|
|
|
|
/**
|
|
* CSRF Redirect
|
|
*
|
|
* Redirect to previous page with error on failure.
|
|
*
|
|
* @var boolean
|
|
*/
|
|
protected $redirect = true;
|
|
|
|
/**
|
|
* 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 string
|
|
*
|
|
* @deprecated
|
|
*/
|
|
protected $samesite = Cookie::SAMESITE_LAX;
|
|
|
|
/**
|
|
* Constructor.
|
|
*
|
|
* Stores our configuration and fires off the init() method to setup
|
|
* initial state.
|
|
*
|
|
* @param App $config
|
|
*/
|
|
public function __construct(App $config)
|
|
{
|
|
/** @var SecurityConfig */
|
|
$security = config('Security');
|
|
|
|
// Store CSRF-related configurations
|
|
$this->tokenName = $security->tokenName ?? $config->CSRFTokenName ?? $this->tokenName;
|
|
$this->headerName = $security->headerName ?? $config->CSRFHeaderName ?? $this->headerName;
|
|
$this->regenerate = $security->regenerate ?? $config->CSRFRegenerate ?? $this->regenerate;
|
|
$rawCookieName = $security->cookieName ?? $config->CSRFCookieName ?? $this->cookieName;
|
|
|
|
/** @var CookieConfig */
|
|
$cookie = config('Cookie');
|
|
|
|
$cookiePrefix = $cookie->prefix ?? $config->cookiePrefix;
|
|
$this->cookieName = $cookiePrefix . $rawCookieName;
|
|
|
|
$expires = $security->expires ?? $config->CSRFExpire ?? 7200;
|
|
|
|
Cookie::setDefaults($cookie);
|
|
$this->cookie = new Cookie($rawCookieName, $this->generateHash(), [
|
|
'expires' => $expires === 0 ? 0 : time() + $expires,
|
|
]);
|
|
}
|
|
|
|
/**
|
|
* CSRF Verify
|
|
*
|
|
* @param RequestInterface $request
|
|
*
|
|
* @return $this|false
|
|
*
|
|
* @throws SecurityException
|
|
*
|
|
* @deprecated Use `CodeIgniter\Security\Security::verify()` instead of using this method.
|
|
*
|
|
* @codeCoverageIgnore
|
|
*/
|
|
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.
|
|
*
|
|
* @codeCoverageIgnore
|
|
*/
|
|
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.
|
|
*
|
|
* @codeCoverageIgnore
|
|
*/
|
|
public function getCSRFTokenName(): string
|
|
{
|
|
return $this->getTokenName();
|
|
}
|
|
|
|
/**
|
|
* CSRF Verify
|
|
*
|
|
* @param RequestInterface $request
|
|
*
|
|
* @return $this|false
|
|
*
|
|
* @throws SecurityException
|
|
*/
|
|
public function verify(RequestInterface $request)
|
|
{
|
|
// If it's not a POST request we will set the CSRF cookie.
|
|
if (strtoupper($_SERVER['REQUEST_METHOD']) !== 'POST')
|
|
{
|
|
return $this->sendCookie($request);
|
|
}
|
|
|
|
// 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]) || ! hash_equals($token, $_COOKIE[$this->cookieName]))
|
|
{
|
|
throw SecurityException::forDisallowedAction();
|
|
}
|
|
|
|
if (isset($_POST[$this->tokenName]))
|
|
{
|
|
// 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);
|
|
}
|
|
elseif (isset($json->{$this->tokenName}))
|
|
{
|
|
// 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));
|
|
}
|
|
|
|
if ($this->regenerate)
|
|
{
|
|
$this->hash = null;
|
|
unset($_COOKIE[$this->cookieName]);
|
|
}
|
|
|
|
$this->cookie = $this->cookie->withValue($this->generateHash());
|
|
$this->sendCookie($request);
|
|
|
|
log_message('info', 'CSRF token verified.');
|
|
|
|
return $this;
|
|
}
|
|
|
|
/**
|
|
* Returns the CSRF Hash.
|
|
*
|
|
* @return string|null
|
|
*/
|
|
public function getHash(): ?string
|
|
{
|
|
return $this->hash;
|
|
}
|
|
|
|
/**
|
|
* Returns the CSRF Token Name.
|
|
*
|
|
* @return string
|
|
*/
|
|
public function getTokenName(): string
|
|
{
|
|
return $this->tokenName;
|
|
}
|
|
|
|
/**
|
|
* Returns the CSRF Header Name.
|
|
*
|
|
* @return string
|
|
*/
|
|
public function getHeaderName(): string
|
|
{
|
|
return $this->headerName;
|
|
}
|
|
|
|
/**
|
|
* Returns the CSRF Cookie Name.
|
|
*
|
|
* @return string
|
|
*/
|
|
public function getCookieName(): string
|
|
{
|
|
return $this->cookieName;
|
|
}
|
|
|
|
/**
|
|
* Check if CSRF cookie is expired.
|
|
*
|
|
* @return boolean
|
|
*
|
|
* @deprecated
|
|
*
|
|
* @codeCoverageIgnore
|
|
*/
|
|
public function isExpired(): bool
|
|
{
|
|
return $this->cookie->isExpired();
|
|
}
|
|
/**
|
|
* Check if request should be redirect on failure.
|
|
*
|
|
* @return boolean
|
|
*/
|
|
public function shouldRedirect(): bool
|
|
{
|
|
return $this->redirect;
|
|
}
|
|
|
|
/**
|
|
* Sanitize Filename
|
|
*
|
|
* Tries to sanitize filenames in order to prevent directory traversal attempts
|
|
* and other security threats, which is particularly useful for files that
|
|
* were supplied via user input.
|
|
*
|
|
* If it is acceptable for the user input to include relative paths,
|
|
* e.g. file/in/some/approved/folder.txt, you can set the second optional
|
|
* parameter, $relative_path to TRUE.
|
|
*
|
|
* @param string $str Input file name
|
|
* @param boolean $relativePath Whether to preserve paths
|
|
*
|
|
* @return string
|
|
*/
|
|
public function sanitizeFilename(string $str, bool $relativePath = false): string
|
|
{
|
|
// List of sanitize filename strings
|
|
$bad = [
|
|
'../',
|
|
'<!--',
|
|
'-->',
|
|
'<',
|
|
'>',
|
|
"'",
|
|
'"',
|
|
'&',
|
|
'$',
|
|
'#',
|
|
'{',
|
|
'}',
|
|
'[',
|
|
']',
|
|
'=',
|
|
';',
|
|
'?',
|
|
'%20',
|
|
'%22',
|
|
'%3c',
|
|
'%253c',
|
|
'%3e',
|
|
'%0e',
|
|
'%28',
|
|
'%29',
|
|
'%2528',
|
|
'%26',
|
|
'%24',
|
|
'%3f',
|
|
'%3b',
|
|
'%3d',
|
|
];
|
|
|
|
if (! $relativePath)
|
|
{
|
|
$bad[] = './';
|
|
$bad[] = '/';
|
|
}
|
|
|
|
$str = remove_invisible_characters($str, false);
|
|
|
|
do
|
|
{
|
|
$old = $str;
|
|
$str = str_replace($bad, '', $str);
|
|
}
|
|
while ($old !== $str);
|
|
|
|
return stripslashes($str);
|
|
}
|
|
|
|
/**
|
|
* 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
|
|
*/
|
|
protected function sendCookie(RequestInterface $request)
|
|
{
|
|
if ($this->cookie->isSecure() && ! $request->isSecure())
|
|
{
|
|
return false;
|
|
}
|
|
|
|
$this->doSendCookie();
|
|
log_message('info', 'CSRF cookie sent.');
|
|
|
|
return $this;
|
|
}
|
|
|
|
/**
|
|
* Actual dispatching of cookies.
|
|
* Extracted for this to be unit tested.
|
|
*
|
|
* @codeCoverageIgnore
|
|
*
|
|
* @return void
|
|
*/
|
|
protected function doSendCookie(): void
|
|
{
|
|
cookies([$this->cookie], false)->dispatch();
|
|
}
|
|
}
|