You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
389 lines
9.3 KiB
389 lines
9.3 KiB
<?php
|
|
|
|
/**
|
|
* CodeIgniter
|
|
*
|
|
* An open source application development framework for PHP
|
|
*
|
|
* This content is released under the MIT License (MIT)
|
|
*
|
|
* Copyright (c) 2014-2019 British Columbia Institute of Technology
|
|
* Copyright (c) 2019 CodeIgniter Foundation
|
|
*
|
|
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
* of this software and associated documentation files (the "Software"), to deal
|
|
* in the Software without restriction, including without limitation the rights
|
|
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
* copies of the Software, and to permit persons to whom the Software is
|
|
* furnished to do so, subject to the following conditions:
|
|
*
|
|
* The above copyright notice and this permission notice shall be included in
|
|
* all copies or substantial portions of the Software.
|
|
*
|
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
* THE SOFTWARE.
|
|
*
|
|
* @package CodeIgniter
|
|
* @author CodeIgniter Dev Team
|
|
* @copyright 2019 CodeIgniter Foundation
|
|
* @license https://opensource.org/licenses/MIT MIT License
|
|
* @link https://codeigniter.com
|
|
* @since Version 4.0.0
|
|
* @filesource
|
|
*/
|
|
|
|
namespace CodeIgniter\Security;
|
|
|
|
use CodeIgniter\HTTP\RequestInterface;
|
|
use CodeIgniter\Security\Exceptions\SecurityException;
|
|
|
|
/**
|
|
* HTTP security handler.
|
|
*/
|
|
class Security
|
|
{
|
|
|
|
/**
|
|
* CSRF Hash
|
|
*
|
|
* Random hash for Cross Site Request Forgery protection cookie
|
|
*
|
|
* @var string
|
|
*/
|
|
protected $CSRFHash;
|
|
|
|
/**
|
|
* 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
|
|
*
|
|
* Token name for Cross Site Request Forgery protection cookie.
|
|
*
|
|
* @var string
|
|
*/
|
|
protected $CSRFTokenName = 'CSRFToken';
|
|
|
|
/**
|
|
* CSRF Header name
|
|
*
|
|
* Token name for Cross Site Request Forgery protection cookie.
|
|
*
|
|
* @var string
|
|
*/
|
|
protected $CSRFHeaderName = 'CSRFToken';
|
|
|
|
/**
|
|
* CSRF Cookie name
|
|
*
|
|
* Cookie name for Cross Site Request Forgery protection cookie.
|
|
*
|
|
* @var string
|
|
*/
|
|
protected $CSRFCookieName = 'CSRFToken';
|
|
|
|
/**
|
|
* CSRF Regenerate
|
|
*
|
|
* If true, the CSRF Token will be regenerated on every request.
|
|
* If false, will stay the same for the life of the cookie.
|
|
*
|
|
* @var boolean
|
|
*/
|
|
protected $CSRFRegenerate = true;
|
|
|
|
/**
|
|
* Typically will be a forward slash
|
|
*
|
|
* @var string
|
|
*/
|
|
protected $cookiePath = '/';
|
|
|
|
/**
|
|
* Set to .your-domain.com for site-wide cookies
|
|
*
|
|
* @var string
|
|
*/
|
|
protected $cookieDomain = '';
|
|
|
|
/**
|
|
* Cookie will only be set if a secure HTTPS connection exists.
|
|
*
|
|
* @var boolean
|
|
*/
|
|
protected $cookieSecure = false;
|
|
|
|
/**
|
|
* List of sanitize filename strings
|
|
*
|
|
* @var array
|
|
*/
|
|
public $filenameBadChars = [
|
|
'../',
|
|
'<!--',
|
|
'-->',
|
|
'<',
|
|
'>',
|
|
"'",
|
|
'"',
|
|
'&',
|
|
'$',
|
|
'#',
|
|
'{',
|
|
'}',
|
|
'[',
|
|
']',
|
|
'=',
|
|
';',
|
|
'?',
|
|
'%20',
|
|
'%22',
|
|
'%3c', // <
|
|
'%253c', // <
|
|
'%3e', // >
|
|
'%0e', // >
|
|
'%28', // (
|
|
'%29', // )
|
|
'%2528', // (
|
|
'%26', // &
|
|
'%24', // $
|
|
'%3f', // ?
|
|
'%3b', // ;
|
|
'%3d', // =
|
|
];
|
|
|
|
//--------------------------------------------------------------------
|
|
|
|
/**
|
|
* Security constructor.
|
|
*
|
|
* Stores our configuration and fires off the init() method to
|
|
* setup initial state.
|
|
*
|
|
* @param \Config\App $config
|
|
*
|
|
* @throws \Exception
|
|
*/
|
|
public function __construct($config)
|
|
{
|
|
// Store our CSRF-related settings
|
|
$this->CSRFExpire = $config->CSRFExpire;
|
|
$this->CSRFTokenName = $config->CSRFTokenName;
|
|
$this->CSRFHeaderName = $config->CSRFHeaderName;
|
|
$this->CSRFCookieName = $config->CSRFCookieName;
|
|
$this->CSRFRegenerate = $config->CSRFRegenerate;
|
|
|
|
if (isset($config->cookiePrefix))
|
|
{
|
|
$this->CSRFCookieName = $config->cookiePrefix . $this->CSRFCookieName;
|
|
}
|
|
|
|
// Store cookie-related settings
|
|
$this->cookiePath = $config->cookiePath;
|
|
$this->cookieDomain = $config->cookieDomain;
|
|
$this->cookieSecure = $config->cookieSecure;
|
|
|
|
$this->CSRFSetHash();
|
|
|
|
unset($config);
|
|
}
|
|
|
|
//--------------------------------------------------------------------
|
|
|
|
/**
|
|
* CSRF Verify
|
|
*
|
|
* @param RequestInterface $request
|
|
*
|
|
* @return $this|false
|
|
* @throws \Exception
|
|
*/
|
|
public function CSRFVerify(RequestInterface $request)
|
|
{
|
|
// If it's not a POST request we will set the CSRF cookie
|
|
if (strtoupper($_SERVER['REQUEST_METHOD']) !== 'POST')
|
|
{
|
|
return $this->CSRFSetCookie($request);
|
|
}
|
|
|
|
// Do the tokens exist in _POST, HEADER or optionally php:://input - json data
|
|
$CSRFTokenValue = $_POST[$this->CSRFTokenName] ??
|
|
(! is_null($request->getHeader($this->CSRFHeaderName)) && ! empty($request->getHeader($this->CSRFHeaderName)->getValue()) ?
|
|
$request->getHeader($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?
|
|
{
|
|
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]))
|
|
{
|
|
unset($_POST[$this->CSRFTokenName]);
|
|
$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}))
|
|
{
|
|
unset($json->{$this->CSRFTokenName});
|
|
$request->setBody(json_encode($json));
|
|
}
|
|
|
|
// Regenerate on every submission?
|
|
if ($this->CSRFRegenerate)
|
|
{
|
|
// Nothing should last forever
|
|
$this->CSRFHash = null;
|
|
unset($_COOKIE[$this->CSRFCookieName]);
|
|
}
|
|
|
|
$this->CSRFSetHash();
|
|
$this->CSRFSetCookie($request);
|
|
|
|
log_message('info', 'CSRF token verified');
|
|
|
|
return $this;
|
|
}
|
|
|
|
//--------------------------------------------------------------------
|
|
|
|
/**
|
|
* CSRF Set Cookie
|
|
*
|
|
* @codeCoverageIgnore
|
|
*
|
|
* @param RequestInterface|\CodeIgniter\HTTP\IncomingRequest $request
|
|
*
|
|
* @return Security|false
|
|
*/
|
|
public function CSRFSetCookie(RequestInterface $request)
|
|
{
|
|
$expire = time() + $this->CSRFExpire;
|
|
$secure_cookie = (bool) $this->cookieSecure;
|
|
|
|
if ($secure_cookie && ! $request->isSecure())
|
|
{
|
|
return false;
|
|
}
|
|
|
|
setcookie(
|
|
$this->CSRFCookieName, $this->CSRFHash, $expire, $this->cookiePath, $this->cookieDomain, $secure_cookie, true // Enforce HTTP only cookie for security
|
|
);
|
|
|
|
log_message('info', 'CSRF cookie sent');
|
|
|
|
return $this;
|
|
}
|
|
|
|
//--------------------------------------------------------------------
|
|
|
|
/**
|
|
* Returns the current CSRF Hash.
|
|
*
|
|
* @return string
|
|
*/
|
|
public function getCSRFHash(): string
|
|
{
|
|
return $this->CSRFHash;
|
|
}
|
|
|
|
//--------------------------------------------------------------------
|
|
|
|
/**
|
|
* Returns the CSRF Token Name.
|
|
*
|
|
* @return string
|
|
*/
|
|
public function getCSRFTokenName(): string
|
|
{
|
|
return $this->CSRFTokenName;
|
|
}
|
|
|
|
//--------------------------------------------------------------------
|
|
|
|
/**
|
|
* Sets the CSRF Hash and cookie.
|
|
*
|
|
* @return string
|
|
* @throws \Exception
|
|
*/
|
|
protected function CSRFSetHash(): string
|
|
{
|
|
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];
|
|
}
|
|
|
|
$rand = random_bytes(16);
|
|
$this->CSRFHash = bin2hex($rand);
|
|
}
|
|
|
|
return $this->CSRFHash;
|
|
}
|
|
|
|
//--------------------------------------------------------------------
|
|
|
|
/**
|
|
* 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 $relative_path Whether to preserve paths
|
|
*
|
|
* @return string
|
|
*/
|
|
public function sanitizeFilename(string $str, bool $relative_path = false): string
|
|
{
|
|
$bad = $this->filenameBadChars;
|
|
|
|
if (! $relative_path)
|
|
{
|
|
$bad[] = './';
|
|
$bad[] = '/';
|
|
}
|
|
|
|
$str = remove_invisible_characters($str, false);
|
|
|
|
do
|
|
{
|
|
$old = $str;
|
|
$str = str_replace($bad, '', $str);
|
|
}
|
|
while ($old !== $str);
|
|
|
|
return stripslashes($str);
|
|
}
|
|
|
|
//--------------------------------------------------------------------
|
|
}
|
|
|