AC_services_website_design/system/Security/Security.php

629 lines
16 KiB
PHP

<?php
/**
* This file is part of 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\IncomingRequest;
use CodeIgniter\HTTP\Request;
use CodeIgniter\HTTP\RequestInterface;
use CodeIgniter\I18n\Time;
use CodeIgniter\Security\Exceptions\SecurityException;
use CodeIgniter\Session\Session;
use Config\Cookie as CookieConfig;
use Config\Security as SecurityConfig;
use Config\Services;
use ErrorException;
use InvalidArgumentException;
use LogicException;
/**
* Class Security
*
* Provides methods that help protect your site against
* Cross-Site Request Forgery attacks.
*
* @see \CodeIgniter\Security\SecurityTest
*/
class Security implements SecurityInterface
{
public const CSRF_PROTECTION_COOKIE = 'cookie';
public const CSRF_PROTECTION_SESSION = 'session';
protected const CSRF_HASH_BYTES = 16;
/**
* CSRF Protection Method
*
* Protection Method for Cross Site Request Forgery protection.
*
* @var string 'cookie' or 'session'
*
* @deprecated 4.4.0 Use $this->config->csrfProtection.
*/
protected $csrfProtection = self::CSRF_PROTECTION_COOKIE;
/**
* CSRF Token Randomization
*
* @var bool
*
* @deprecated 4.4.0 Use $this->config->tokenRandomize.
*/
protected $tokenRandomize = false;
/**
* CSRF Hash (without randomization)
*
* Random hash for Cross Site Request Forgery protection.
*
* @var string|null
*/
protected $hash;
/**
* CSRF Token Name
*
* Token name for Cross Site Request Forgery protection.
*
* @var string
*
* @deprecated 4.4.0 Use $this->config->tokenName.
*/
protected $tokenName = 'csrf_token_name';
/**
* CSRF Header Name
*
* Header name for Cross Site Request Forgery protection.
*
* @var string
*
* @deprecated 4.4.0 Use $this->config->headerName.
*/
protected $headerName = 'X-CSRF-TOKEN';
/**
* The CSRF Cookie instance.
*
* @var Cookie
*/
protected $cookie;
/**
* CSRF Cookie Name (with Prefix)
*
* Cookie name for Cross Site Request Forgery protection.
*
* @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 int
*
* @deprecated 4.4.0 Use $this->config->expires.
*/
protected $expires = 7200;
/**
* CSRF Regenerate
*
* Regenerate CSRF Token on every request.
*
* @var bool
*
* @deprecated 4.4.0 Use $this->config->regenerate.
*/
protected $regenerate = true;
/**
* CSRF Redirect
*
* Redirect to previous page with error on failure.
*
* @var bool
*
* @deprecated 4.4.0 Use $this->config->redirect.
*/
protected $redirect = false;
/**
* 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 `Config\Cookie` $samesite property is used.
*/
protected $samesite = Cookie::SAMESITE_LAX;
private IncomingRequest $request;
/**
* CSRF Cookie Name without Prefix
*/
private ?string $rawCookieName = null;
/**
* Session instance.
*/
private ?Session $session = null;
/**
* CSRF Hash in Request Cookie
*
* The cookie value is always CSRF hash (without randomization) even if
* $tokenRandomize is true.
*/
private ?string $hashInCookie = null;
/**
* Security Config
*/
protected SecurityConfig $config;
/**
* Constructor.
*
* Stores our configuration and fires off the init() method to setup
* initial state.
*/
public function __construct(SecurityConfig $config)
{
$this->config = $config;
$this->rawCookieName = $config->cookieName;
if ($this->isCSRFCookie()) {
$cookie = config(CookieConfig::class);
$this->configureCookie($cookie);
} else {
// Session based CSRF protection
$this->configureSession();
}
$this->request = Services::request();
$this->hashInCookie = $this->request->getCookie($this->cookieName);
$this->restoreHash();
if ($this->hash === null) {
$this->generateHash();
}
}
private function isCSRFCookie(): bool
{
return $this->config->csrfProtection === self::CSRF_PROTECTION_COOKIE;
}
private function configureSession(): void
{
$this->session = Services::session();
}
private function configureCookie(CookieConfig $cookie): void
{
$cookiePrefix = $cookie->prefix;
$this->cookieName = $cookiePrefix . $this->rawCookieName;
Cookie::setDefaults($cookie);
}
/**
* CSRF Verify
*
* @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 Token.
*
* @deprecated Use `CodeIgniter\Security\Security::getHash()` instead of using this method.
*
* @codeCoverageIgnore
*/
public function getCSRFHash(): ?string
{
return $this->getHash();
}
/**
* Returns the CSRF Token Name.
*
* @deprecated Use `CodeIgniter\Security\Security::getTokenName()` instead of using this method.
*
* @codeCoverageIgnore
*/
public function getCSRFTokenName(): string
{
return $this->getTokenName();
}
/**
* CSRF Verify
*
* @return $this
*
* @throws SecurityException
*/
public function verify(RequestInterface $request)
{
// Protects POST, PUT, DELETE, PATCH
$method = strtoupper($request->getMethod());
$methodsToProtect = ['POST', 'PUT', 'DELETE', 'PATCH'];
if (! in_array($method, $methodsToProtect, true)) {
return $this;
}
$postedToken = $this->getPostedToken($request);
try {
$token = ($postedToken !== null && $this->config->tokenRandomize)
? $this->derandomize($postedToken) : $postedToken;
} catch (InvalidArgumentException $e) {
$token = null;
}
// Do the tokens match?
if (! isset($token, $this->hash) || ! hash_equals($this->hash, $token)) {
throw SecurityException::forDisallowedAction();
}
$this->removeTokenInRequest($request);
if ($this->config->regenerate) {
$this->generateHash();
}
log_message('info', 'CSRF token verified.');
return $this;
}
/**
* Remove token in POST or JSON request data
*/
private function removeTokenInRequest(RequestInterface $request): void
{
assert($request instanceof Request);
if (isset($_POST[$this->config->tokenName])) {
// We kill this since we're done and we don't want to pollute the POST array.
unset($_POST[$this->config->tokenName]);
$request->setGlobal('post', $_POST);
} else {
$body = $request->getBody() ?? '';
$json = json_decode($body);
if ($json !== null && json_last_error() === JSON_ERROR_NONE) {
// We kill this since we're done and we don't want to pollute the JSON data.
unset($json->{$this->config->tokenName});
$request->setBody(json_encode($json));
} else {
parse_str($body, $parsed);
// We kill this since we're done and we don't want to pollute the BODY data.
unset($parsed[$this->config->tokenName]);
$request->setBody(http_build_query($parsed));
}
}
}
private function getPostedToken(RequestInterface $request): ?string
{
assert($request instanceof IncomingRequest);
// Does the token exist in POST, HEADER or optionally php:://input - json data or PUT, DELETE, PATCH - raw data.
if ($tokenValue = $request->getPost($this->config->tokenName)) {
return $tokenValue;
}
if ($request->hasHeader($this->config->headerName)
&& $request->header($this->config->headerName)->getValue() !== ''
&& $request->header($this->config->headerName)->getValue() !== []) {
return $request->header($this->config->headerName)->getValue();
}
$body = (string) $request->getBody();
if ($body !== '') {
$json = json_decode($body);
if ($json !== null && json_last_error() === JSON_ERROR_NONE) {
return $json->{$this->config->tokenName} ?? null;
}
parse_str($body, $parsed);
return $parsed[$this->config->tokenName] ?? null;
}
return null;
}
/**
* Returns the CSRF Token.
*/
public function getHash(): ?string
{
return $this->config->tokenRandomize ? $this->randomize($this->hash) : $this->hash;
}
/**
* Randomize hash to avoid BREACH attacks.
*
* @params string $hash CSRF hash
*
* @return string CSRF token
*/
protected function randomize(string $hash): string
{
$keyBinary = random_bytes(static::CSRF_HASH_BYTES);
$hashBinary = hex2bin($hash);
if ($hashBinary === false) {
throw new LogicException('$hash is invalid: ' . $hash);
}
return bin2hex(($hashBinary ^ $keyBinary) . $keyBinary);
}
/**
* Derandomize the token.
*
* @params string $token CSRF token
*
* @return string CSRF hash
*
* @throws InvalidArgumentException "hex2bin(): Hexadecimal input string must have an even length"
*/
protected function derandomize(string $token): string
{
$key = substr($token, -static::CSRF_HASH_BYTES * 2);
$value = substr($token, 0, static::CSRF_HASH_BYTES * 2);
try {
return bin2hex(hex2bin($value) ^ hex2bin($key));
} catch (ErrorException $e) {
// "hex2bin(): Hexadecimal input string must have an even length"
throw new InvalidArgumentException($e->getMessage());
}
}
/**
* Returns the CSRF Token Name.
*/
public function getTokenName(): string
{
return $this->config->tokenName;
}
/**
* Returns the CSRF Header Name.
*/
public function getHeaderName(): string
{
return $this->config->headerName;
}
/**
* Returns the CSRF Cookie Name.
*/
public function getCookieName(): string
{
return $this->config->cookieName;
}
/**
* Check if CSRF cookie is expired.
*
* @deprecated
*
* @codeCoverageIgnore
*/
public function isExpired(): bool
{
return $this->cookie->isExpired();
}
/**
* Check if request should be redirect on failure.
*/
public function shouldRedirect(): bool
{
return $this->config->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 bool $relativePath Whether to preserve paths
*/
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);
}
/**
* Restore hash from Session or Cookie
*/
private function restoreHash(): void
{
if ($this->isCSRFCookie()) {
if ($this->isHashInCookie()) {
$this->hash = $this->hashInCookie;
}
} elseif ($this->session->has($this->config->tokenName)) {
// Session based CSRF protection
$this->hash = $this->session->get($this->config->tokenName);
}
}
/**
* Generates (Regenerates) the CSRF Hash.
*/
public function generateHash(): string
{
$this->hash = bin2hex(random_bytes(static::CSRF_HASH_BYTES));
if ($this->isCSRFCookie()) {
$this->saveHashInCookie();
} else {
// Session based CSRF protection
$this->saveHashInSession();
}
return $this->hash;
}
private function isHashInCookie(): bool
{
if ($this->hashInCookie === null) {
return false;
}
$length = static::CSRF_HASH_BYTES * 2;
$pattern = '#^[0-9a-f]{' . $length . '}$#iS';
return preg_match($pattern, $this->hashInCookie) === 1;
}
private function saveHashInCookie(): void
{
$this->cookie = new Cookie(
$this->rawCookieName,
$this->hash,
[
'expires' => $this->config->expires === 0 ? 0 : Time::now()->getTimestamp() + $this->config->expires,
]
);
$response = Services::response();
$response->setCookie($this->cookie);
}
/**
* CSRF Send Cookie
*
* @return false|Security
*
* @deprecated Set cookies to Response object instead.
*/
protected function sendCookie(RequestInterface $request)
{
assert($request instanceof IncomingRequest);
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
*
* @deprecated Set cookies to Response object instead.
*/
protected function doSendCookie(): void
{
cookies([$this->cookie], false)->dispatch();
}
private function saveHashInSession(): void
{
$this->session->set($this->config->tokenName, $this->hash);
}
}