first commit
This commit is contained in:
@@ -0,0 +1,463 @@
|
||||
<?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();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user