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

<?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);
}
//--------------------------------------------------------------------
}