CVE-2016-7400 (published 2017-02-07T15:59Z)

Multiple SQL injection vulnerabilities in Exponent CMS before 2.4.0 allow remote attackers to execute arbitrary SQL commands via the (1) id parameter in an activate_address address controller action, (2) title parameter in a show blog controller action, or (3) content_id parameter in a showComments expComment controller action.

Severity: HIGH
CVSS v2 base score: 7.5 (AV:N/AC:L/Au:N/C:P/I:P/A:P)
CVSS v3 base score: 9.8, CRITICAL (CVSS:3.0/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H)

Weakness information:
cwe_id: CWE-89
cwe_name: Improper Neutralization of Special Elements used in an SQL Command ('SQL Injection')
description: The product constructs all or part of an SQL command using externally-influenced input from an upstream component, but it does not neutralize or incorrectly neutralizes special elements that could modify the intended SQL command when it is sent to a downstream component. Without sufficient removal or quoting of SQL syntax in user-controllable inputs, the generated SQL query can cause those inputs to be interpreted as SQL instead of ordinary user data.
extended_description: 
            
            
         
         

Fix information:
repo_url: https://github.com/exponentcms/exponent-cms
commit_details: repo_url: https://github.com/exponentcms/exponent-cms
author: dleffler
author_date: 2016-09-13 15:38:24-04:00
msg: security fix several sql injection vulnerabilities reported by Manuel Garcia Cardenas and PKAV TEAM
repository: repo_url: https://github.com/exponentcms/exponent-cms
repo_name: exponentcms/exponent-cms
description: Content Management, Simple.
repo_language: PHP
owner: exponentcms
file_changes: filename: mysqli.php
change_type: ModificationType.MODIFY
diff: @@ -379,6 +379,8 @@ class mysqli_database extends database {
     function selectObjects($table, $where = null, $orderby = null) {
         if ($where == null)
             $where = "1";
+        else
+            $where = $this->injectProof($where);
         if ($orderby == null)
             $orderby = '';
         else
@@ -484,7 +486,7 @@ class mysqli_database extends database {
         //$lfh = fopen($logFile, 'a');
         //fwrite($lfh, $sql . "\n");    
         //fclose($lfh);                 
-        $res = @mysqli_query($this->connection, $sql);
+        $res = @mysqli_query($this->connection, $this->injectProof($sql));
         if ($res == null)
             return null;
         return mysqli_fetch_object($res);
@@ -497,7 +499,7 @@ class mysqli_database extends database {
 	 * @return array
 	 */
     function selectObjectsBySql($sql) {
-        $res = @mysqli_query($this->connection, $sql);
+        $res = @mysqli_query($this->connection, $this->injectProof($sql));
         if ($res == null)
             return array();
         $objects = array();
@@ -638,6 +640,8 @@ class mysqli_database extends database {
     function selectObjectsIndexedArray($table, $where = null, $orderby = null) {
         if ($where == null)
             $where = "1";
+        else
+            $where = $this->injectProof($where);
         if ($orderby == null)
             $orderby = '';
         else
@@ -722,6 +726,7 @@ class mysqli_database extends database {
      * @return object/null|void
      */
     function selectObject($table, $where) {
+        $where = $this->injectProof($where);
         $res = mysqli_query($this->connection, "SELECT * FROM `" . $this->prefix . "$table` WHERE $where LIMIT 0,1");
         if ($res == null)
             return null;
@@ -773,7 +778,7 @@ class mysqli_database extends database {
                 if ($values != ") VALUES (") {
                     $values .= ",";
                 }
-                $values .= "'" . mysqli_real_escape_string($this->connection, $val) . "'";
+                $values .= "'" . $this->escapeString($val) . "'";
             }
         }
         $sql = substr($sql, 0, -1) . substr($values, 0) . ")";
@@ -836,13 +841,13 @@ class mysqli_database extends database {
                     $val = serialize($val);   
                     $sql .= "`$var`='".$val."',";
                 } else {
-                    $sql .= "`$var`='".mysqli_real_escape_string($this->connection,$val)."',";
+                    $sql .= "`$var`='" . $this->escapeString($val) . "',";
                 }
             }
         }
         $sql = substr($sql, 0, -1) . " WHERE ";
         if ($where != null)
-            $sql .= $where;
+            $sql .= $this->injectProof($where);
         else
             $sql .= "`" . $identifier . "`=" . $object->$identifier;
         //if ($table == 'text') eDebug($sql,true);        
@@ -1130,6 +1135,8 @@ class mysqli_database extends database {
     function selectArrays($table, $where = null, $orderby = null) {
         if ($where == null)
             $where = "1";
+        else
+            $where = $this->injectProof($where);
         if ($orderby == null)
             $orderby = '';
         else
@@ -1156,7 +1163,7 @@ class mysqli_database extends database {
      * @return array
      */
     function selectArraysBySql($sql) {        
-        $res = @mysqli_query($this->connection, $sql);
+        $res = @mysqli_query($this->connection, $this->injectProof($sql));
         if ($res == null)
             return array();
         $arrays = array();
@@ -1185,6 +1192,8 @@ class mysqli_database extends database {
     function selectArray($table, $where = null, $orderby = null, $is_revisioned=false, $needs_approval=false) {
         if ($where == null)
             $where = "1";
+        else
+            $where = $this->injectProof($where);
         $as = '';
         if ($is_revisioned) {
    //            $where.= " AND revision_id=(SELECT MAX(revision_id) FROM `" . $this->prefix . "$table` WHERE $where)";
@@ -1223,6 +1232,8 @@ class mysqli_database extends database {
     function selectExpObjects($table, $where=null, $classname, $get_assoc=true, $get_attached=true, $except=array(), $cascade_except=false, $order=null, $limitsql=null, $is_revisioned=false, $needs_approval=false) {
         if ($where == null)
             $where = "1";
+        else
+            $where = $this->injectProof($where);
         $as = '';
         if ($is_revisioned) {
    //            $where.= " AND revision_id=(SELECT MAX(revision_id) FROM `" . $this->prefix . "$table` WHERE $where)";
@@ -1259,7 +1270,7 @@ class mysqli_database extends database {
      * @return array
      */
     function selectExpObjectsBySql($sql, $classname, $get_assoc=true, $get_attached=true) {
-        $res = @mysqli_query($this->connection, $sql);
+        $res = @mysqli_query($this->connection, $this->injectProof($sql));
         if ($res == null)
             return array();
         $arrays = array();

programming_language: PHP
filename: expDatabase.php
change_type: ModificationType.MODIFY
diff: @@ -1105,6 +1105,21 @@ abstract class database {
 	 */
 	abstract function escapeString($string);
 
+    /**
+   	 * Unescape a string based on the database connection
+   	 * @param $string
+   	 * @return string
+   	 */
+   	function injectProof($string) {
+   	    $quotes = substr_count("'", $string);
+        if ($quotes % 2 != 0)
+            $string = $this->escapeString($string);
+        $dquotes = substr_count('"', $string);
+        if ($dquotes % 2 != 0)
+            $string = $this->escapeString($string);
+        return $string;
+    }
+
 	/**
 	 * Create a SQL "limit" phrase
 	 *

programming_language: PHP
filename: expRouter.php
change_type: ModificationType.MODIFY
diff: @@ -198,11 +198,26 @@ class expRouter {
                 );
             }
         }
-        // conventional method to ensure the 'id' is an id
-        if (isset($_GET['id'])) {
-            $_GET['id'] = intval($_GET['id']);
+        // conventional method to ensure the 'id' is only an id
+        if (isset($_REQUEST['id'])) {
+            if (isset($_GET['id']))
+                $_GET['id'] = intval($_GET['id']);
+            if (isset($_POST['id']))
+                $_POST['id'] = intval($_POST['id']);
+
             $_REQUEST['id'] = intval($_REQUEST['id']);
         }
+        // do the same for the other id's
+        foreach ($_REQUEST as $key=>$var) {
+            if (is_string($var) && strrpos($key,'_id',-3) !== false) {
+                if (isset($_GET[$key]))
+                    $_GET[$key] = intval($_GET[$key]);
+                if (isset($_POST[$key]))
+                    $_POST[$key] = intval($_POST[$key]);
+
+                $_REQUEST[$key] = intval($_REQUEST[$key]);
+            }
+        }
         if (empty($user->id) || (!empty($user->id) && !$user->isAdmin())) {  //FIXME why would $user be empty here unless $db is down?
 //            $_REQUEST['route_sanitized'] = true;//FIXME debug test
             expString::sanitize($_REQUEST);  // strip other exploits like sql injections
@@ -307,14 +322,14 @@ class expRouter {
             
             if (count($this->url_parts) < 1 || (empty($this->url_parts[0]) && count($this->url_parts) == 1) ) {
                 $this->url_type = 'base';  // no params
-            } elseif (count($this->url_parts) == 1 || $db->selectObject('section', "sef_name='" . substr($db->escapeString($this->sefPath),1) . "'") != null) {
+            } elseif (count($this->url_parts) == 1 || $db->selectObject('section', "sef_name='" . substr($this->sefPath,1) . "'") != null) {
                 $this->url_type = 'page';  // single param is page name
             } elseif ($_SERVER['REQUEST_METHOD'] == 'POST') {
                 $this->url_type = 'post';  // params via form/post
             } else {
                 // take a peek and see if a page exists with the same name as the first value...if so we probably have a page with
                 // extra perms...like printerfriendly=1 or ajax_action=1;
-                if (($db->selectObject('section', "sef_name='" . $db->escapeString($this->url_parts[0]) . "'") != null) && (in_array(array('printerfriendly','exportaspdf','ajax_action'), $this->url_parts))) {
+                if (($db->selectObject('section', "sef_name='" . $this->url_parts[0] . "'") != null) && (in_array(array('printerfriendly','exportaspdf','ajax_action'), $this->url_parts))) {
                     $this->url_type = 'page';
                 } else {
                     $this->url_type = 'action';
@@ -547,7 +562,7 @@ class expRouter {
         } else {
             $url .= urldecode((empty($_SERVER['REQUEST_URI'])) ? $_ENV['REQUEST_URI'] : $_SERVER['REQUEST_URI']);
         }
-        return expString::sanitize($url);
+        return expString::escape(expString::sanitize($url));
     }
 
     public static function encode($url) {
@@ -760,7 +775,7 @@ class expRouter {
         if (substr($this->sefPath,-1) == "/") $this->sefPath = substr($this->sefPath,0,-1);
         // sanitize it
         $sefPath = explode('">',$this->sefPath);  // remove any attempts to close the command
-        $this->sefPath = expString::sanitize($sefPath[0]);
+        $this->sefPath = expString::escape(expString::sanitize($sefPath[0]));
     }
 
     public function getSection() {

programming_language: PHP
filename: expString.php
change_type: ModificationType.MODIFY
diff: @@ -522,23 +522,23 @@ class expString {
             $data = self::xss_clean($data);
 
             //fixme orig exp method
-            if(0) {
-                // remove whitespaces and tags
-//            $data = strip_tags(trim($data));
-                // remove whitespaces and script tags
-                $data = self::strip_tags_content(trim($data), '<script>', true);
-//            $data = self::strip_tags_content(trim($data), '<iframe>', true);
-
-                // apply stripslashes if magic_quotes_gpc is enabled
-                if (get_magic_quotes_gpc()) {
-                    $data = stripslashes($data);
-                }
-
-                $data = self::escape($data);
-
-                // re-escape newlines
-                $data = str_replace(array('\r', '\n'), array("\r", "\n"), $data);
-            }
+//            if(0) {
+//                // remove whitespaces and tags
+////            $data = strip_tags(trim($data));
+//                // remove whitespaces and script tags
+//                $data = self::strip_tags_content(trim($data), '<script>', true);
+////            $data = self::strip_tags_content(trim($data), '<iframe>', true);
+//
+//                // apply stripslashes if magic_quotes_gpc is enabled
+//                if (get_magic_quotes_gpc()) {
+//                    $data = stripslashes($data);
+//                }
+//
+//                $data = self::escape($data);
+//
+//                // re-escape newlines
+//                $data = str_replace(array('\r', '\n'), array("\r", "\n"), $data);
+//            }
         }
         return $data;
     }

programming_language: PHP
filename: expTheme.php
change_type: ModificationType.MODIFY
diff: @@ -762,7 +762,7 @@ class expTheme
             if (!$user->isAdmin())
                 expString::sanitize($_REQUEST);
 //            } elseif (empty($_REQUEST['array_sanitized'])) {
-                $tmp =1;  //FIXME we've already sanitized at this point
+//                $tmp =1;  //FIXME we've already sanitized at this point
 //            } else {
 //                $tmp =1;  //FIXME we've already sanitized at this point
 //            }
@@ -843,7 +843,8 @@ class expTheme
 ////                $_GET[$key] = $value;
 //                $_GET[$key] = expString::sanitize($value);
 //            }
-            if (!$user->isAdmin()) expString::sanitize($_GET);
+            if (!$user->isAdmin())
+                expString::sanitize($_GET);
         }
         //if (isset($['_common'])) $actfile = "/common/actions/" . $_REQUEST['action'] . ".php";
 

programming_language: PHP