CVE-2021-29436: Cross-Site Request Forgery (CSRF) (CWE-352)
value: Anuko Time Tracker is an open source, web-based time tracking application written in PHP. In Time Tracker before version 1.19.27.5431 a Cross site request forgery (CSRF) vulnerability existed. The nature of CSRF is that a logged on user may be tricked by social engineering to click on an attacker-provided form that executes an unintended action such as changing user password. The vulnerability is fixed in Time Tracker version 1.19.27.5431. Upgrade is recommended. If upgrade is not practical, introduce ttMitigateCSRF() function in /WEB-INF/lib/common.php.lib using the latest available code and call it from ttAccessAllowed().
CWE description: The web application does not, or can not, sufficiently verify whether a well-formed, valid, consistent request was intentionally provided by the user who submitted the request.
CVSS v3 base score: 8.1
Published: 2021-04-13T20:15Z
Repository: https://github.com/anuko/timetracker
Language: PHP
Files changed: WEB-INF/config.php.dist, WEB-INF/lib/common.lib.php, initialize.php
Commit date: 2021-04-11 21:57:58 +0000
Commit message: An attempt to mitigate cross site request forgery vulnerability.

Diff: diff --git a/WEB-INF/config.php.dist b/WEB-INF/config.php.dist
index 7b6eabbd2..1b7b32b0a 100644
--- a/WEB-INF/config.php.dist
+++ b/WEB-INF/config.php.dist
@@ -161,10 +161,15 @@ define('AUTH_MODULE', 'db');
                                         // array('Ldap Testers', 'Ldap Users') means the user must be a member of both Ldap Testers and Ldap Users groups.
 
 // define('DEBUG', false); // Note: enabling DEBUG breaks redirects as debug output is printed before setting redirect header. Do not enable on production systems.
 
 
+// HTTP_TARGET - defines http target for cross site request forgery protection.
+// It can be used when you access the application via a proxy.
+// define('HTTP_TARGET', 'localhost');
+
+
 // Group managers can set monthly work hour quota for years between the following  values.
 // define('MONTHLY_QUOTA_YEAR_START', 2010); // If nothing is specified, it falls back to 2015.
 // define('MONTHLY_QUOTA_YEAR_END', 2025);   // If nothing is specified, it falls back to 2030.
 
 // Height in pixels for the note input field in time.php. Defaults to 40.
diff --git a/WEB-INF/lib/common.lib.php b/WEB-INF/lib/common.lib.php
index 389b29c94..52589c6e0 100644
--- a/WEB-INF/lib/common.lib.php
+++ b/WEB-INF/lib/common.lib.php
@@ -361,10 +361,14 @@ function ttAccessAllowed($required_right)
   if (!$auth->isAuthenticated()) {
     header('Location: login.php');
     exit();
   }
 
+  // Protection against cross site request forgery.
+  if (!ttMitigateCSRF())
+    return false;
+
   // Check IP restriction, if set.
   if ($user->allow_ip && !$user->can('override_allow_ip')) {
     $access_allowed = false;
     $user_ip = $_SERVER['REMOTE_ADDR'];
     $allowed_ip_array = explode(',', $user->allow_ip);
@@ -386,10 +390,45 @@ function ttAccessAllowed($required_right)
   }
 
   return false;
 }
 
+// ttMitigateCSRF verifies request headers in an attempt to block cross site request forgery.
+function ttMitigateCSRF() {
+  // No need to do anything for get requests.
+  global $request;
+  if ($request->isGet())
+    return true;
+
+  $origin = $_SERVER['HTTP_ORIGIN'];
+  if ($origin) {
+    $pos = strpos($origin, '//');
+    $origin = substr($origin, $pos+2); // Strip protocol.
+  }
+  if (!$origin) {
+    // Try using referer.
+    $origin = $_SERVER['HTTP_REFERER'];
+    if ($origin) {
+      $pos = strpos($origin, '//');
+      $origin = substr($origin, $pos+2); // Strip protocol.
+      $pos = strpos($origin, '/');
+      $origin = substr($origin, 0, $pos); // Leave host only.
+    }
+  }
+  error_log("origin: ".$origin);
+  $target = defined('HTTP_TARGET') ? HTTP_TARGET : $_SERVER['HTTP_HOST'];
+  error_log("target: ".$target);
+  if (strcmp($origin, $target)) {
+    error_log("Potential cross site request forgery. Origin: '$origin' does not match target: '$target'.");
+    return false; // Origin and target do not match,
+  }
+
+  // TODO: review and improve this function for custom ports.
+  return true;
+}
+
+
 // ttStartsWith functions checks if a string starts with a given substring.
 function ttStartsWith($string, $startString)
 {
     $len = strlen($startString);
     return (substr($string, 0, $len) === $startString);
diff --git a/initialize.php b/initialize.php
index cbdfb3cc1..aadc0bb07 100644
--- a/initialize.php
+++ b/initialize.php
@@ -11,11 +11,11 @@ error_reporting(E_ALL & ~E_NOTICE & ~E_STRICT); // & ~E_DEPRECATED);
 
 // Disable displaying errors on screen.
 ini_set('display_errors', 'Off');
 
 // require_once('init_auth.php');
-define("APP_VERSION", "1.19.26.5430");
+define("APP_VERSION", "1.19.27.5431");
 define("APP_DIR", dirname(__FILE__));
 define("LIBRARY_DIR", APP_DIR."/WEB-INF/lib");
 define("TEMPLATE_DIR", APP_DIR."/WEB-INF/templates");
 // Date format for database and URI parameters.
 define('DB_DATEFORMAT', '%Y-%m-%d');

Vulnerable code: define("APP_VERSION", "1.19.26.5430");
Fixed code: // HTTP_TARGET - defines http target for cross site request forgery protection.
// It can be used when you access the application via a proxy.
// define('HTTP_TARGET', 'localhost');


  // Protection against cross site request forgery.
  if (!ttMitigateCSRF())
    return false;

// ttMitigateCSRF verifies request headers in an attempt to block cross site request forgery.
function ttMitigateCSRF() {
  // No need to do anything for get requests.
  global $request;
  if ($request->isGet())
    return true;

  $origin = $_SERVER['HTTP_ORIGIN'];
  if ($origin) {
    $pos = strpos($origin, '//');
    $origin = substr($origin, $pos+2); // Strip protocol.
  }
  if (!$origin) {
    // Try using referer.
    $origin = $_SERVER['HTTP_REFERER'];
    if ($origin) {
      $pos = strpos($origin, '//');
      $origin = substr($origin, $pos+2); // Strip protocol.
      $pos = strpos($origin, '/');
      $origin = substr($origin, 0, $pos); // Leave host only.
    }
  }
  error_log("origin: ".$origin);
  $target = defined('HTTP_TARGET') ? HTTP_TARGET : $_SERVER['HTTP_HOST'];
  error_log("target: ".$target);
  if (strcmp($origin, $target)) {
    error_log("Potential cross site request forgery. Origin: '$origin' does not match target: '$target'.");
    return false; // Origin and target do not match,
  }

  // TODO: review and improve this function for custom ports.
  return true;
}


define("APP_VERSION", "1.19.27.5431");