diff --git a/src/Config/config.security.php b/src/Config/config.security.php
index 47749b9..346fcbc 100644
--- a/src/Config/config.security.php
+++ b/src/Config/config.security.php
@@ -51,8 +51,14 @@ return [
*/
'csrf_protection' => true,
'csrf_token_name' => 'fw_csrf_token',
- 'csrf_cookie_name' => 'fw_csrf_cookie',
'csrf_expire' => 7200,
- 'csrf_regenerate' => TRUE,
'csrf_exclude_uris' => array(),
+
+ // CSRF Cookie information
+ 'csrf_cookie_name' => 'fw_csrf_cookie',
+ 'csrf_cookie_prefix' => '',
+ 'csrf_cookie_domain' => '',
+ 'csrf_cookie_path' => '/',
+ 'csrf_cookie_secure' => false,
+ 'csrf_cookie_httponly' => false
];
\ No newline at end of file
diff --git a/src/FuzeWorks/Exception/CSRFException.php b/src/FuzeWorks/Exception/CSRFException.php
new file mode 100644
index 0000000..9ffbb13
--- /dev/null
+++ b/src/FuzeWorks/Exception/CSRFException.php
@@ -0,0 +1,42 @@
+webConfig->get('xss_clean');
+ // Never run XSS clean if disabled by config
+ $xssClean = ($this->webConfig->get('xss_clean') == true ? $xssClean : false);
// If the index is null, the entire array is requested
$index = (!is_null($index) ? $index : array_keys($this->inputArray[$arrayName]));
@@ -376,5 +376,28 @@ class Input
return $this->getFromInputArray('server', 'REQUEST_METHOD', $xssClean);
}
+ /**
+ * Is HTTPS?
+ *
+ * Determines if the application is accessed via an encrypted
+ * (HTTPS) connection.
+ *
+ * @return bool
+ */
+ public function isHttps(): bool
+ {
+ if (!empty($this->inputArray['server']['HTTPS']) && strtolower($this->inputArray['server']['HTTPS']) !== 'off')
+ return true;
+
+ elseif (isset($this->inputArray['server']['HTTP_X_FORWARDED_PROTO']) && $this->inputArray['server']['HTTP_X_FORWARDED_PROTO'] === 'https')
+ return true;
+
+ elseif ( ! empty($this->inputArray['server']['HTTP_FRONT_END_HTTPS']) && strtolower($this->inputArray['server']['HTTP_FRONT_END_HTTPS']) !== 'off')
+ return true;
+
+ return false;
+ }
+
+
}
\ No newline at end of file
diff --git a/src/FuzeWorks/Output.php b/src/FuzeWorks/Output.php
index cfd66e7..b77197d 100644
--- a/src/FuzeWorks/Output.php
+++ b/src/FuzeWorks/Output.php
@@ -163,6 +163,9 @@ class Output
ob_start('ob_gzhandler');
}
+ // Remove the X-Powered-By header, since it's a security risk
+ header_remove("X-Powered-By");
+
// Send all available headers
if (!empty($this->headers))
foreach ($this->headers as $header)
diff --git a/src/FuzeWorks/Security.php b/src/FuzeWorks/Security.php
index 8398f80..99be132 100644
--- a/src/FuzeWorks/Security.php
+++ b/src/FuzeWorks/Security.php
@@ -37,7 +37,7 @@
namespace FuzeWorks;
use FuzeWorks\ConfigORM\ConfigORM;
-use FuzeWorks\Exception\{ConfigException, SecurityException, Exception};
+use FuzeWorks\Exception\{ConfigException, CSRFException, Exception};
/**
* Security Class
@@ -169,6 +169,13 @@ class Security {
*/
private $config;
+ /**
+ * Input. A dependency for this class
+ *
+ * @var Input
+ */
+ private $input;
+
/**
* Class constructor
*
@@ -178,6 +185,7 @@ class Security {
public function init()
{
$this->config = Factory::getInstance()->config->get('security');
+ $this->input = Factory::getInstance()->input;
// Is CSRF protection enabled?
if ($this->config->csrf_protection)
@@ -210,14 +218,13 @@ class Security {
* CSRF Verify
*
* @return self
+ * @throws CSRFException
*/
public function csrf_verify(): self
{
// If it's not a POST request we will set the CSRF cookie
- if (strtoupper($_SERVER['REQUEST_METHOD']) !== 'POST')
- {
+ if (strtoupper($this->input->server('REQUEST_METHOD')) !== 'POST')
return $this->csrf_set_cookie();
- }
// Check if URI has been whitelisted from CSRF checks
if ($exclude_uris = $this->config->csrf_exclude_uris)
@@ -232,22 +239,10 @@ class Security {
}
// Do the tokens exist in both the _POST and _COOKIE arrays?
- if ( ! isset($_POST[$this->_csrf_token_name], $_COOKIE[$this->_csrf_cookie_name])
- OR $_POST[$this->_csrf_token_name] !== $_COOKIE[$this->_csrf_cookie_name]) // Do the tokens match?
- {
+ $token = $this->input->post($this->_csrf_token_name);
+ $cookie = $this->input->cookie($this->_csrf_cookie_name);
+ if ($token == null || $cookie == null || $token !== $cookie)
$this->csrf_show_error();
- }
-
- // We kill this since we're done and we don't want to polute the _POST array
- unset($_POST[$this->_csrf_token_name]);
-
- // Regenerate on every submission?
- if ($this->config->csrf_regenerate)
- {
- // Nothing should last forever
- unset($_COOKIE[$this->_csrf_cookie_name]);
- $this->_csrf_hash = NULL;
- }
$this->_csrf_set_hash();
$this->csrf_set_cookie();
@@ -258,6 +253,22 @@ class Security {
// --------------------------------------------------------------------
+ /**
+ * CSRF Regenerate
+ *
+ * @throws ConfigException
+ * @return self
+ */
+ public function csrf_regenerate()
+ {
+ Logger::log("CSRF Hash is being regenerated...");
+ $this->_csrf_hash = null;
+ $this->_csrf_set_hash();
+ $this->csrf_set_cookie();
+
+ return $this;
+ }
+
/**
* CSRF Set Cookie
*
@@ -267,24 +278,25 @@ class Security {
public function csrf_set_cookie()
{
$expire = time() + $this->_csrf_expire;
- $cfg = Factory::getInstance()->config->get('main');
- $secure_cookie = (bool) $cfg->cookie_secure;
+ $cfg = Factory::getInstance()->config->get('security');
+ $secure_cookie = (bool) $cfg->csrf_cookie_secure;
- if ($secure_cookie && ! Core::isHttps())
- {
+ if ($secure_cookie && !$this->input->isHttps())
return $this;
- }
setcookie(
$this->_csrf_cookie_name,
$this->_csrf_hash,
$expire,
- $cfg->cookie_path,
- $cfg->cookie_domain,
+ $cfg->csrf_cookie_path,
+ $cfg->csrf_cookie_domain,
$secure_cookie,
- $cfg->cookie_httponly
+ $cfg->csrf_cookie_httponly
);
+
Logger::log('CSRF cookie sent');
+
+ return $this;
}
// --------------------------------------------------------------------
@@ -293,11 +305,11 @@ class Security {
* Show CSRF Error
*
* @return void
- * @throws SecurityException
+ * @throws CSRFException
*/
public function csrf_show_error()
{
- throw new SecurityException('The action you have requested is not allowed.', 1);
+ throw new CSRFException('This action resulted in a Cross Site Reference Forgery warning. Request will be blocked...', 5);
}
// --------------------------------------------------------------------
@@ -728,57 +740,6 @@ class Security {
return $str;
}
- // --------------------------------------------------------------------
-
- /**
- * Sanitize Filename
- *
- * @param string $str Input file name
- * @param bool $relative_path Whether to preserve paths
- * @return string
- */
- public function sanitize_filename($str, $relative_path = FALSE): string
- {
- $bad = $this->filename_bad_chars;
-
- if ( ! $relative_path)
- {
- $bad[] = './';
- $bad[] = '/';
- }
-
- $str = UTF8::removeInvisibleCharacters($str, FALSE);
-
- do
- {
- $old = $str;
- $str = str_replace($bad, '', $str);
- }
- while ($old !== $str);
-
- return stripslashes($str);
- }
-
- // ----------------------------------------------------------------
-
- /**
- * Strip Image Tags
- *
- * @param string $str
- * @return string
- */
- public function strip_image_tags($str): string
- {
- return preg_replace(
- array(
- '##i',
- '#`]+)).*?\>#i'
- ),
- '\\2',
- $str
- );
- }
-
// ----------------------------------------------------------------
/**
diff --git a/src/FuzeWorks/URI.php b/src/FuzeWorks/URI.php
index d135c5c..29fa54b 100644
--- a/src/FuzeWorks/URI.php
+++ b/src/FuzeWorks/URI.php
@@ -84,7 +84,7 @@ class URI
$scriptName = $this->input->server('SCRIPT_NAME');
$scriptFilename = $this->input->server('SCRIPT_FILENAME');
- $baseUrl = ($this->isHttps() ? 'https' : 'http') .
+ $baseUrl = ($this->input->isHttps() ? 'https' : 'http') .
"://" . $serverAddr .
substr($scriptName, 0, strpos($scriptName, basename($scriptFilename)));
}
@@ -222,31 +222,4 @@ class URI
return true;
}
- /**
- * Is HTTPS?
- *
- * Determines if the application is accessed via an encrypted
- * (HTTPS) connection.
- *
- * @return bool
- */
- protected function isHttps(): bool
- {
- $https = $this->input->server('HTTPS');
- if (!is_null($https) && strtolower($https) !== 'off')
- {
- return true;
- }
- elseif (!is_null($this->input->server('HTTP_X_FORWARDED_PROTO')) && $this->input->server('HTTP_X_FORWARDED_PROTO') === 'https')
- {
- return true;
- }
- elseif ( ! is_null($this->input->server('HTTP_FRONT_END_HTTPS')) && strtolower($this->input->server('HTTP_FRONT_END_HTTPS')) !== 'off')
- {
- return true;
- }
-
- return false;
- }
-
}
\ No newline at end of file
diff --git a/src/FuzeWorks/WebComponent.php b/src/FuzeWorks/WebComponent.php
index a87a443..f40723d 100644
--- a/src/FuzeWorks/WebComponent.php
+++ b/src/FuzeWorks/WebComponent.php
@@ -36,12 +36,17 @@
namespace FuzeWorks;
+use FuzeWorks\Event\HaltExecutionEvent;
+use FuzeWorks\Event\LayoutLoadEvent;
+use FuzeWorks\Event\RouterCallViewEvent;
+use FuzeWorks\Exception\CSRFException;
use FuzeWorks\Exception\EventException;
use FuzeWorks\Exception\Exception;
use FuzeWorks\Exception\HaltException;
use FuzeWorks\Exception\NotFoundException;
use FuzeWorks\Exception\OutputException;
use FuzeWorks\Exception\RouterException;
+use FuzeWorks\Exception\SecurityException;
use FuzeWorks\Exception\WebException;
class WebComponent implements iComponent
@@ -100,6 +105,9 @@ class WebComponent implements iComponent
{
// First init UTF8
UTF8::init();
+
+ // Register some base events
+ Events::addListener([$this, 'layoutLoadEventListener'], 'layoutLoadEvent', Priority::NORMAL);
}
/**
@@ -137,7 +145,7 @@ class WebComponent implements iComponent
try {
// Set the output to display when shutting down
- Events::addListener(function () {
+ Events::addListener(function ($event) {
/** @var Output $output */
Logger::logInfo("Parsing output...");
$output = Factory::getInstance()->output;
@@ -153,9 +161,25 @@ class WebComponent implements iComponent
/** @var Router $router */
/** @var URI $uri */
/** @var Output $output */
+ /** @var Security $security */
$router = Factory::getInstance()->router;
$uri = Factory::getInstance()->uri;
$output = Factory::getInstance()->output;
+ $security = Factory::getInstance()->security;
+
+ // And start logging the request
+ Logger::newLevel("Routing web request...");
+
+ // First test for Cross Site Request Forgery
+ try {
+ $security->csrf_verify();
+ } catch (SecurityException $exception) {
+ // If a SecurityException is thrown, first log it
+ Logger::logWarning("SecurityException thrown. Registering listener to verify handler in View");
+
+ // Register a listener
+ Events::addListener([$this, 'callViewEventListener'], 'routerCallViewEvent', Priority::HIGHEST, $exception);
+ }
// Attempt to load the requested page
try {
@@ -165,6 +189,9 @@ class WebComponent implements iComponent
Logger::logWarning("Requested page not found. Requesting Error/error404 View");
$output->setStatusHeader(404);
+ // Remove listener so that error pages won't be intercepted
+ Events::removeListener([$this, 'callViewEventListener'], 'routerCallViewEvent',Priority::HIGHEST);
+
// Request 404 page=
try {
$viewOutput = $router->route('Error/error404');
@@ -175,15 +202,54 @@ class WebComponent implements iComponent
Logger::exceptionHandler($e, false);
$viewOutput = 'ERROR 404. Page was not found.';
}
+ } catch (HaltException $e) {
+ Logger::logWarning("Requested page was denied. Requesting Error/error403 View.");
+ $output->setStatusHeader(403);
+
+ // Remove listener so that error pages won't be intercepted
+ Events::removeListener([$this, 'callViewEventListener'], 'routerCallViewEvent',Priority::HIGHEST);
+
+ try {
+ $viewOutput = $router->route('Error/error403');
+ } catch (NotFoundException $e) {
+ // If still resulting in an error, do something else
+ $viewOutput = 'ERROR 403. Forbidden.';
+ } catch (Exception $e) {
+ Logger::exceptionHandler($e, false);
+ $viewOutput = 'ERROR 403. Forbidden.';
+ }
}
// Append the output
if (!empty($viewOutput))
$output->appendOutput($viewOutput);
+ Logger::stopLevel();
return true;
}
+ /**
+ * Listener for routerCallViewEvent
+ *
+ * Fired when a SecurityException is thrown. Verifies if a securityExceptionHandler() method exists.
+ * If not, the calling of the view is cancelled. If yes, the calling of the view depends on the
+ * result of the method
+ *
+ * @param RouterCallViewEvent $event
+ * @param SecurityException $exception
+ */
+ public function callViewEventListener(RouterCallViewEvent $event, SecurityException $exception)
+ {
+ /** @var RouterCallViewEvent $event */
+ // If the securityExceptionHandler method exists, cancel based on that methods output
+ if (method_exists($event->view, 'securityExceptionHandler'))
+ $event->setCancelled(!$event->view->securityExceptionHandler($exception));
+
+ // If not, cancel it immediately
+ else
+ $event->setCancelled(true);
+ }
+
/**
* Listener for haltExecutionEvent
*
@@ -191,7 +257,7 @@ class WebComponent implements iComponent
*
* @param $event
*/
- public function haltEventListener($event)
+ public function haltEventListener(HaltExecutionEvent $event)
{
// Dependencies
/** @var Output $output */
@@ -203,6 +269,9 @@ class WebComponent implements iComponent
// Cancel event
$event->setCancelled(true);
+ // Remove listener so that error pages won't be intercepted
+ Events::removeListener([$this, 'callViewEventListener'], 'routerCallViewEvent',Priority::HIGHEST);
+
try {
// And handle consequences
Logger::logError("Execution halted. Providing error 500 page.");
@@ -216,4 +285,28 @@ class WebComponent implements iComponent
// Finally append output and shutdown
$output->appendOutput($viewOutput);
}
+
+ /**
+ * Listener for layoutLoadEvent
+ *
+ * Assigns variables from the WebComponent to Layout engines.
+ *
+ * @param $event
+ * @throws Exception\ConfigException
+ */
+ public function layoutLoadEventListener(LayoutLoadEvent $event)
+ {
+ // Dependencies
+ /** @var Security $security */
+ /** @var Config $config */
+ $security = Factory::getInstance()->security;
+ $config = Factory::getInstance()->config;
+
+ /** @var LayoutLoadEvent $event */
+ $event->assign('csrfHash', $security->get_csrf_hash());
+ $event->assign('csrfTokenName', $security->get_csrf_token_name());
+ $event->assign('siteURL', $config->getConfig('web')->get('base_url'));
+
+ Logger::logInfo("Assigned variables to TemplateEngine from WebComponent");
+ }
}
\ No newline at end of file