VYPR
Critical severityNVD Advisory· Published Nov 21, 2022· Updated Apr 14, 2025

SQL Injection in dolibarr/dolibarr

CVE-2022-4093

Description

SQL injection attacks can result in unauthorized access to sensitive data, such as passwords, credit card details, or personal user information. Many high-profile data breaches in recent years have been the result of SQL injection attacks, leading to reputational damage and regulatory fines. In some cases, an attacker can obtain a persistent backdoor into an organization's systems, leading to a long-term compromise that can go unnoticed for an extended period. This affect 16.0.1 and 16.0.2 only. 16.0.0 or lower, and 16.0.3 or higher are not affected

AI Insight

LLM-synthesized narrative grounded in this CVE's description and references.

Dolibarr ERP/CRM versions 16.0.1 and 16.0.2 are vulnerable to SQL injection, allowing unauthorized data access and potential persistent compromise.

Vulnerability

Overview

CVE-2022-4093 describes a SQL injection vulnerability present in Dolibarr ERP/CRM versions 16.0.1 and 16.0.2. The issue stems from insufficient sanitization of user-supplied input when building SQL queries, specifically in the handling of LIKE operations. A commit message [2] indicates the fix involved ensuring proper escaping after the escapeForLike function, suggesting that dynamic query parameters were not adequately parameterized, allowing an attacker to inject arbitrary SQL fragments.

Exploitation and

Attack Surface

The vulnerability can be exploited over a network without requiring authentication, as the attack vector is network-based with low attack complexity [1]. An attacker would need to craft a malicious HTTP request containing SQL meta-characters, likely through a parametered URL or form field that is directly concatenated into a SQL query. No special privileges or user interaction are required, making the attack surface broad and accessible to unauthenticated remote attackers.

Impact

Successful exploitation enables an attacker to perform arbitrary SQL queries against the underlying database. This could lead to unauthorized retrieval of sensitive data such as user credentials, financial information, and other personally identifiable information (PII) [1]. In many documented cases, SQL injection has been used to establish persistent backdoors, potentially allowing long-term undetected compromise of the system [1].

Mitigation

Status

The vulnerability is patched in Dolibarr version 16.0.3 and later [1]. Users running versions 16.0.1 or 16.0.2 should upgrade immediately to a fixed release. For those unable to upgrade, a code patch [2] is publicly available that implements proper escaping in the affected code path. No workarounds are documented, but input validation and parameterized queries would also mitigate the issue.

AI Insight generated on May 21, 2026. Synthesized from this CVE's description and the cited reference URLs; citations are validated against the source bundle.

Affected packages

Versions sourced from the GitHub Security Advisory.

PackageAffected versionsPatched versions
dolibarr/dolibarrPackagist
>= 16.0.1, < 16.0.316.0.3

Affected products

3

Patches

1
7c1eac9774bd

Fix sqli ->escape after ->escapeforlike

https://github.com/dolibarr/dolibarrLaurent DestailleurNov 21, 2022via ghsa
5 files changed · +194 12
  • htdocs/contact/list.php+4 3 modified
    @@ -502,16 +502,17 @@
     		if ($value['active'] && strlen($search_[$key])) {
     			$searchkeyinjsonformat = preg_replace('/"$/', '', preg_replace('/^"/', '', json_encode($search_[$key])));
     			if (in_array($db->type, array('mysql', 'mysqli'))) {
    -				$sql .= " AND p.socialnetworks REGEXP '\"".$db->escapeforlike($db->escape($key))."\":\"[^\"]*".$db->escapeforlike($db->escape($searchkeyinjsonformat))."'";
    +				$sql .= " AND p.socialnetworks REGEXP '\"".$db->escape($db->escapeforlike($key))."\":\"[^\"]*".$db->escape($db->escapeforlike($searchkeyinjsonformat))."'";
     			} elseif ($db->type == 'pgsql') {
    -				$sql .= " AND p.socialnetworks ~ '\"".$db->escapeforlike($db->escape($key))."\":\"[^\"]*".$db->escapeforlike($db->escape($searchkeyinjsonformat))."'";
    +				$sql .= " AND p.socialnetworks ~ '\"".$db->escape($db->escapeforlike($key))."\":\"[^\"]*".$db->escape($db->escapeforlike($searchkeyinjsonformat))."'";
     			} else {
     				// Works with all database but not reliable because search only for social network code starting with earched value
    -				$sql .= " AND p.socialnetworks LIKE '%\"".$db->escapeforlike($db->escape($key))."\":\"".$db->escapeforlike($db->escape($searchkeyinjsonformat))."%'";
    +				$sql .= " AND p.socialnetworks LIKE '%\"".$db->escape($db->escapeforlike($key))."\":\"".$db->escape($db->escapeforlike($searchkeyinjsonformat))."%'";
     			}
     		}
     	}
     }
    +//print $sql;
     if (strlen($search_email)) {
     	$sql .= natural_search('p.email', $search_email);
     }
    
  • htdocs/core/lib/website.lib.php+8 5 modified
    @@ -887,7 +887,7 @@ function getSocialNetworkSharingLinks()
      * @param	string		$langcode			Language code ('' or 'en', 'fr', 'es', ...)
      * @param	array		$otherfilters		Other filters
      * @param	int			$status				0 or 1, or -1 for both
    - * @return  string							HTML content
    + * @return  array							Array with results of search
      */
     function getPagesFromSearchCriterias($type, $algo, $searchstring, $max = 25, $sortfield = 'date_creation', $sortorder = 'DESC', $langcode = '', $otherfilters = 'null', $status = 1)
     {
    @@ -925,6 +925,8 @@ function getPagesFromSearchCriterias($type, $algo, $searchstring, $max = 25, $so
     	$found = 0;
     
     	if (!$error && (empty($max) || ($found < $max)) && (preg_match('/meta/', $algo) || preg_match('/content/', $algo))) {
    +		include_once DOL_DOCUMENT_ROOT.'/website/class/websitepage.class.php';
    +
     		$sql = 'SELECT wp.rowid FROM '.MAIN_DB_PREFIX.'website_page as wp';
     		if (is_array($otherfilters) && !empty($otherfilters['category'])) {
     			$sql .= ', '.MAIN_DB_PREFIX.'categorie_website_page as cwp';
    @@ -934,7 +936,7 @@ function getPagesFromSearchCriterias($type, $algo, $searchstring, $max = 25, $so
     			$sql .= " AND wp.status = ".((int) $status);
     		}
     		if ($langcode) {
    -			$sql .= " AND wp.lang ='".$db->escape($langcode)."'";
    +			$sql .= " AND wp.lang = '".$db->escape($langcode)."'";
     		}
     		if ($type) {
     			$tmparrayoftype = explode(',', $type);
    @@ -947,11 +949,11 @@ function getPagesFromSearchCriterias($type, $algo, $searchstring, $max = 25, $so
     		$sql .= " AND (";
     		$searchalgo = '';
     		if (preg_match('/meta/', $algo)) {
    -			$searchalgo .= ($searchalgo ? ' OR ' : '')."wp.title LIKE '%".$db->escapeforlike($db->escape($searchstring))."%' OR wp.description LIKE '%".$db->escapeforlike($db->escape($searchstring))."%'";
    -			$searchalgo .= ($searchalgo ? ' OR ' : '')."wp.keywords LIKE '".$db->escapeforlike($db->escape($searchstring)).",%' OR wp.keywords LIKE '% ".$db->escapeforlike($db->escape($searchstring))."%'"; // TODO Use a better way to scan keywords
    +			$searchalgo .= ($searchalgo ? ' OR ' : '')."wp.title LIKE '%".$db->escape($db->escapeforlike($searchstring))."%' OR wp.description LIKE '%".$db->escape($db->escapeforlike($searchstring))."%'";
    +			$searchalgo .= ($searchalgo ? ' OR ' : '')."wp.keywords LIKE '".$db->escape($db->escapeforlike($searchstring)).",%' OR wp.keywords LIKE '% ".$db->escape($db->escapeforlike($searchstring))."%'"; // TODO Use a better way to scan keywords
     		}
     		if (preg_match('/content/', $algo)) {
    -			$searchalgo .= ($searchalgo ? ' OR ' : '')."wp.content LIKE '%".$db->escapeforlike($db->escape($searchstring))."%'";
    +			$searchalgo .= ($searchalgo ? ' OR ' : '')."wp.content LIKE '%".$db->escape($db->escapeforlike($searchstring))."%'";
     		}
     		$sql .= $searchalgo;
     		if (is_array($otherfilters) && !empty($otherfilters['category'])) {
    @@ -963,6 +965,7 @@ function getPagesFromSearchCriterias($type, $algo, $searchstring, $max = 25, $so
     		//print $sql;
     
     		$resql = $db->query($sql);
    +
     		if ($resql) {
     			$i = 0;
     			while (($obj = $db->fetch_object($resql)) && ($i < $max || $max == 0)) {
    
  • htdocs/core/modules/import/import_csv.modules.php+2 2 modified
    @@ -862,8 +862,8 @@ public function import_insert($arrayrecord, $array_match_file_to_database, $obji
     										$stringtosearch = json_encode($socialnetwork).':'.json_encode($json->$socialnetwork);
     										//var_dump($stringtosearch);
     										//var_dump($this->db->escape($stringtosearch));	// This provide a value for sql string (but not for a like)
    -										$where[] = $key." LIKE '%".$this->db->escapeforlike($this->db->escape($stringtosearch))."%'";
    -										$filters[] = $col." LIKE '%".$this->db->escapeforlike($this->db->escape($stringtosearch))."%'";
    +										$where[] = $key." LIKE '%".$this->db->escape($this->db->escapeforlike($stringtosearch))."%'";
    +										$filters[] = $col." LIKE '%".$this->db->escape($this->db->escapeforlike($stringtosearch))."%'";
     										//var_dump($where[1]); // This provide a value for sql string inside a like
     									} else {
     										$where[] = $key.' = '.$data[$key];
    
  • htdocs/core/modules/import/import_xlsx.modules.php+2 2 modified
    @@ -908,8 +908,8 @@ public function import_insert($arrayrecord, $array_match_file_to_database, $obji
     										$stringtosearch = json_encode($socialnetwork).':'.json_encode($json->$socialnetwork);
     										//var_dump($stringtosearch);
     										//var_dump($this->db->escape($stringtosearch));	// This provide a value for sql string (but not for a like)
    -										$where[] = $key." LIKE '%".$this->db->escapeforlike($this->db->escape($stringtosearch))."%'";
    -										$filters[] = $col." LIKE '%".$this->db->escapeforlike($this->db->escape($stringtosearch))."%'";
    +										$where[] = $key." LIKE '%".$this->db->escape($this->db->escapeforlike($stringtosearch))."%'";
    +										$filters[] = $col." LIKE '%".$this->db->escape($this->db->escapeforlike($stringtosearch))."%'";
     										//var_dump($where[1]); // This provide a value for sql string inside a like
     									} else {
     										$where[] = $key.' = '.$data[$key];
    
  • test/phpunit/Website.class.php+178 0 added
    @@ -0,0 +1,178 @@
    +<?php
    +/* Copyright (C) 2010 Laurent Destailleur  <eldy@users.sourceforge.net>
    + *
    + * This program is free software; you can redistribute it and/or modify
    + * it under the terms of the GNU General Public License as published by
    + * the Free Software Foundation; either version 3 of the License, or
    + * (at your option) any later version.
    + *
    + * This program is distributed in the hope that it will be useful,
    + * but WITHOUT ANY WARRANTY; without even the implied warranty of
    + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    + * GNU General Public License for more details.
    + *
    + * You should have received a copy of the GNU General Public License
    + * along with this program. If not, see <https://www.gnu.org/licenses/>.
    + * or see https://www.gnu.org/
    + */
    +
    +/**
    + *      \file       test/phpunit/WebsiteTest.php
    + *		\ingroup    test
    + *      \brief      PHPUnit test
    + *		\remarks	To run this script as CLI:  phpunit filename.php
    + */
    +
    +global $conf,$user,$langs,$db;
    +//define('TEST_DB_FORCE_TYPE','mysql');	// This is to force using mysql driver
    +//require_once 'PHPUnit/Autoload.php';
    +
    +if (! defined('NOREQUIRESOC')) {
    +	define('NOREQUIRESOC', '1');
    +}
    +if (! defined('NOCSRFCHECK')) {
    +	define('NOCSRFCHECK', '1');
    +}
    +if (! defined('NOTOKENRENEWAL')) {
    +	define('NOTOKENRENEWAL', '1');
    +}
    +if (! defined('NOREQUIREMENU')) {
    +	define('NOREQUIREMENU', '1'); // If there is no menu to show
    +}
    +if (! defined('NOREQUIREHTML')) {
    +	define('NOREQUIREHTML', '1'); // If we don't need to load the html.form.class.php
    +}
    +if (! defined('NOREQUIREAJAX')) {
    +	define('NOREQUIREAJAX', '1');
    +}
    +if (! defined("NOLOGIN")) {
    +	define("NOLOGIN", '1');       // If this page is public (can be called outside logged session)
    +}
    +if (! defined("NOSESSION")) {
    +	define("NOSESSION", '1');
    +}
    +
    +require_once dirname(__FILE__).'/../../htdocs/main.inc.php';
    +require_once dirname(__FILE__).'/../../htdocs/core/lib/website.lib.php';
    +
    +
    +if (empty($user->id)) {
    +	print "Load permissions for admin user nb 1\n";
    +	$user->fetch(1);
    +	$user->getrights();
    +}
    +$conf->global->MAIN_DISABLE_ALL_MAILS=1;
    +
    +
    +/**
    + * Class for PHPUnit tests
    + *
    + * @backupGlobals disabled
    + * @backupStaticAttributes enabled
    + * @remarks	backupGlobals must be disabled to have db,conf,user and lang not erased.
    + */
    +class WebsiteTest extends PHPUnit\Framework\TestCase
    +{
    +	protected $savconf;
    +	protected $savuser;
    +	protected $savlangs;
    +	protected $savdb;
    +
    +	/**
    +	 * Constructor
    +	 * We save global variables into local variables
    +	 *
    +	 * @return SecurityTest
    +	 */
    +	public function __construct()
    +	{
    +		parent::__construct();
    +
    +		//$this->sharedFixture
    +		global $conf,$user,$langs,$db;
    +		$this->savconf=$conf;
    +		$this->savuser=$user;
    +		$this->savlangs=$langs;
    +		$this->savdb=$db;
    +
    +		print __METHOD__." db->type=".$db->type." user->id=".$user->id;
    +		//print " - db ".$db->db;
    +		print "\n";
    +	}
    +
    +	/**
    +	 * setUpBeforeClass
    +	 *
    +	 * @return void
    +	 */
    +	public static function setUpBeforeClass()
    +	{
    +		global $conf,$user,$langs,$db;
    +		$db->begin();	// This is to have all actions inside a transaction even if test launched without suite.
    +
    +		print __METHOD__."\n";
    +	}
    +
    +	/**
    +	 * tearDownAfterClass
    +	 *
    +	 * @return	void
    +	 */
    +	public static function tearDownAfterClass()
    +	{
    +		global $conf,$user,$langs,$db;
    +		$db->rollback();
    +
    +		print __METHOD__."\n";
    +	}
    +
    +	/**
    +	 * Init phpunit tests
    +	 *
    +	 * @return	void
    +	 */
    +	protected function setUp()
    +	{
    +		global $conf,$user,$langs,$db;
    +		$conf=$this->savconf;
    +		$user=$this->savuser;
    +		$langs=$this->savlangs;
    +		$db=$this->savdb;
    +
    +		print __METHOD__."\n";
    +	}
    +
    +	/**
    +	 * End phpunit tests
    +	 *
    +	 * @return	void
    +	 */
    +	protected function tearDown()
    +	{
    +		print __METHOD__."\n";
    +	}
    +
    +
    +	/**
    +	 * testGetPagesFromSearchCriterias
    +	 *
    +	 * @return	void
    +	 */
    +	public function testGetPagesFromSearchCriterias()
    +	{
    +		global $db;
    +
    +		$s = "123') OR 1=1-- \' xxx";
    +		/*
    +		var_dump($s);
    +		var_dump($db->escapeforlike($s));
    +		var_dump($db->escape($db->escapeforlike($s)));
    +		*/
    +
    +		$res = getPagesFromSearchCriterias('page,blogpost', 'meta,content', $s, 2, 'date_creation', 'DESC', 'en');
    +		//var_dump($res);
    +		print __METHOD__." message=".$res['code']."\n";
    +		// We must found no line (so code should be KO). If we found somethiing, it means there is a SQL injection of the 1=1
    +		$this->assertEquals($res['code'], 'KO');
    +	}
    +}
    

Vulnerability mechanics

Generated on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.

References

4

News mentions

0

No linked articles in our index yet.