YesWiki: Unauthenticated SQL Injection
Description
Summary
An unauthenticated SQL injection in the Bazar form-import path (FormManager::create()) allows any unauthenticated visitor of a default YesWiki install to inject arbitrary SQL into an INSERT statement and read the full database, including yeswiki_users.password hashes. Present in 4.6.1 / 4.6.2 / current doryphore-dev; analyzed against upstream commit 1f485c049db030b94c047ec219e63534ac81142e.
Details
Sink is at FormManager::create() (function at L232), unquoted concatenation of bn_id_nature into the INSERT VALUES list at https://github.com/YesWiki/yeswiki/blob/1f485c049db030b94c047ec219e63534ac81142e/tools/bazar/services/FormManager.php#L258
Reachability is unauthenticated.
PoC
1. Clone the repo (test was done on 1f485c049db030b94c047ec219e63534ac81142e) 2. Bring up the service using docker: cd docker && docker compose build && docker compose up 3. Go to https://localhost:8085 4. Go through the installation 5. Run the POC: yeswiki_sqli_poc.py
Impact
Sql injection. An attacker can dump the whole db, including usernames, emails, and hashed passwords.
More details
Sample http request (copied from burp): `` POST /?BazaR&vue=formulaire HTTP/1.1 Accept-Encoding: gzip, deflate, br Content-Length: 353 Host: localhost:8085 User-Agent: Python-urllib/3.13 Content-Type: application/x-www-form-urlencoded Connection: keep-alive imported-form%5B7791000%2BASCII%28SUBSTRING%28%28SELECT%2F%2A%2A%2FHEX%28CONCAT%28email%2C0x3a%2Cpassword%29%29%2F%2A%2A%2FFROM%2F%2A%2A%2Fyeswiki_users%2F%2A%2A%2FLIMIT%2F%2A%2A%2F1%29%2C1%2C1%29%29%5D=%7B%22bn_label_nature%22%3A+%22zz_poc_7790000_1%22%2C+%22bn_template%22%3A+%22%22%2C+%22bn_description%22%3A+%22%22%2C+%22bn_condition%22%3A+%22%22%7D ``
#### POC internals: The PoC uses an expression like: 7330000 + ASCII(SUBSTRING((SELECT HEX(VERSION())), 1, 1))
Breakdown SELECT HEX(VERSION()) or whatever the statement is (the poc file dumps 1 username and password) This gets the database version and hex-encodes it. Example: `` VERSION() = 9.7.0 HEX(VERSION()) = 392E372E30 ``
Then: SUBSTRING((SELECT HEX(VERSION())), 1, 1) takes one character from that hex string. For position 1, this returns 3, then: ASCII(...) converts that character to its ASCII code: ASCII('3') = 51 Then: 7330000 + 51 produces 7330051 So the full vulnerable insert becomes roughly: `` INSERT INTO yeswiki_nature (..., bn_id_nature, ...) VALUES (7330000 + ASCII(SUBSTRING((SELECT HEX(VERSION())), 1, 1)), "fr-FR", ...); ``
MySQL evaluates the expression before storing it, so the inserted row has: bn_id_nature = 7330051 The PoC reads that ID from /?api/forms, subtracts 7330000, gets 51, converts 51 back to '3', and repeats for the next character.
AI Insight
LLM-synthesized narrative grounded in this CVE's description and references.
An unauthenticated SQL injection in YesWiki's Bazar form-import path allows any visitor to dump the entire database, including password hashes.
Vulnerability
CVE-2026-46670 is an unauthenticated SQL injection vulnerability in the Bazar form-import path of YesWiki. The sink resides in the FormManager::create() function (line 232), where the bn_id_nature parameter is concatenated without sanitization into an INSERT statement's VALUES list at line 258 of tools/bazar/services/FormManager.php. This affects versions 4.6.1, 4.6.2, and the current doryphore-dev branch, analyzed against upstream commit 1f485c049db030b94c047ec219e63534ac81142e [1][2]. No authentication or special configuration is required; a default YesWiki installation is vulnerable.
Exploitation
An unauthenticated attacker can send a crafted POST request to the /?BazaR&vue=formulaire endpoint with a malicious imported-form parameter. The provided proof-of-concept (PoC) uses an expression like 7330000 + ASCII(SUBSTRING((SELECT HEX(CONCAT(email,0x3a,password)) FROM yeswiki_users LIMIT 1),1,1)) as the array key to extract one character at a time via blind injection [1][2]. The attacker needs only network access to the target and does not require any user interaction or prior authentication.
Impact
Successful exploitation allows an unauthenticated attacker to perform a blind SQL injection, enabling them to read the entire database. This includes sensitive information such as usernames, email addresses, and password hashes from the yeswiki_users table. The attacker can dump data character by character, leading to full disclosure of user credentials and potential account compromise [1][2].
Mitigation
As of the publication date of this advisory, no patched version has been released for YesWiki. The vulnerability is present in the development branch (doryphore-dev). No known workaround is provided in the available references [1][2]. Users should monitor the YesWiki GitHub repository and advisory channels for a security release. The CVE has not been listed in CISA's Known Exploited Vulnerabilities (KEV) catalog.
AI Insight generated on May 22, 2026. Synthesized from this CVE's description and the cited reference URLs; citations are validated against the source bundle.
Affected products
2Patches
6ba141823f04ffix(bazar): use intval instead of mysqli_real_escape_string for id
1 file changed · +1 −1
tools/bazar/services/FormManager.php+1 −1 modified@@ -303,7 +303,7 @@ public function update($data) . ($this->isAvailableOnlyOneEntryOption() ? '`bn_only_one_entry`="' . ((isset($data['bn_only_one_entry']) && $data['bn_only_one_entry'] === 'Y') ? 'Y' : 'N') . '",' : '') . ($this->isAvailableOnlyOneEntryMessage() ? '`bn_only_one_entry_message`="' . (empty($data['bn_only_one_entry_message']) ? '' : $this->dbService->escape(_convert($data['bn_only_one_entry_message'], YW_CHARSET, true))) . '",' : '') . '`bn_condition`="' . $this->dbService->escape(_convert($data['bn_condition'], YW_CHARSET, true)) . '"' - . ' WHERE `bn_id_nature`=' . $this->dbService->escape($data['bn_id_nature'])); + . ' WHERE `bn_id_nature`=' . intval($data['bn_id_nature'])); } public function clone($id)
fe7244bffcbefix(bazar): harden import forms + escape form ids
2 files changed · +4 −1
tools/bazar/controllers/FormController.php+3 −0 modified@@ -36,6 +36,9 @@ public function displayAll($message) // If there are forms to import if (isset($_POST['imported-form'])) { + if (!$this->getService(Guard::class)->isAllowed('saisie_formulaire')) { + return $this->wiki->redirect($this->wiki->href('', '', ['vue' => 'formulaire', 'msg' => 'BAZ_AUTH_NEEDED'], false)); + } foreach ($_POST['imported-form'] as $id => $value) { $value = json_decode($value, true); $existingForms = multiArraySearch($forms, 'bn_label_nature', $value['bn_label_nature']);
tools/bazar/services/FormManager.php+1 −1 modified@@ -255,7 +255,7 @@ public function create($data) . ($this->isAvailableOnlyOneEntryOption() ? ',`bn_only_one_entry`' : '') . ($this->isAvailableOnlyOneEntryMessage() ? ',`bn_only_one_entry_message`' : '') . ',`bn_condition`)' - . ' VALUES (' . $data['bn_id_nature'] . ', "fr-FR", "' + . ' VALUES (' . intval($data['bn_id_nature']) . ', "fr-FR", "' . $this->dbService->escape(_convert($data['bn_label_nature'] ?? '', YW_CHARSET, true)) . '", "' . $this->dbService->escape(_convert($data['bn_template'] ?? '', YW_CHARSET, true)) . '", "' . $this->dbService->escape(_convert($data['bn_description'] ?? '', YW_CHARSET, true)) . '", "'
03f27fdde5f8fix(bazar): harden test for api/entries
1 file changed · +1 −1
tools/bazar/controllers/ApiController.php+1 −1 modified@@ -216,7 +216,7 @@ public function getWebfinger(Request $request) */ public function getAllFormEntries($formId, $output = null, $selectedEntries = null) { - if (strpos($formId, 'b64_') === 0) { + if (!is_array($formId) && strpos($formId, 'b64_') === 0) { $vFormID = base64_decode(urldecode(substr($formId, 4)), true); } else { $vFormID = $formId;
6f222915df36fix(core): catch errors in sql queries
1 file changed · +14 −19
includes/services/DbService.php+14 −19 modified@@ -4,9 +4,7 @@ use DateInterval; use DateTime; -use Exception; use Symfony\Component\DependencyInjection\ParameterBag\ParameterBagInterface; -use Throwable; class DbService { @@ -36,7 +34,7 @@ protected function initSqlConnection() $this->params->has('mysql_port') ? $this->params->get('mysql_port') : ini_get('mysqli.default_port') ); if (!$this->link) { - throw new Exception('Not connected to sql'); + throw new \Exception('Not connected to sql'); } if ($this->params->has('db_charset') and $this->params->get('db_charset') === 'utf8mb4') { // necessaire pour les versions de mysql qui ont un autre encodage par defaut @@ -51,12 +49,11 @@ protected function initSqlConnection() $this->collation = (mysqli_character_set_name($this->link) === 'utf8mb4') ? 'utf8mb4_unicode_ci' : 'utf8_unicode_ci'; - } catch (Throwable $th) { + } catch (\Throwable $th) { if (in_array(php_sapi_name(), ['cli', 'cli-server', ' phpdbg'], true)) { - throw new Exception(_t('DB_CONNECT_FAIL')); - } else { - exit(_t('DB_CONNECT_FAIL')); + throw new \Exception(_t('DB_CONNECT_FAIL')); } + exit(_t('DB_CONNECT_FAIL')); } } @@ -110,13 +107,11 @@ public function query($query) try { if (!$result = mysqli_query($this->link, $query)) { - throw new Exception('Query failed: ' . $query . ' (' . mysqli_error($this->link) . ')'); + throw new \Exception('Query failed: ' . $query . ' (' . mysqli_error($this->link) . ')'); } - }/* - catch (Exception $e) { - file_put_contents ("log.txt", $query, FILE_APPEND); - }*/ - finally { + } catch (\Exception $e) { + $this->addQueryLog('ERROR IN QUERY : ' . $query, $this->getMicroTime() - $start); + } finally { if ($this->params->get('debug')) { $this->addQueryLog($query, $this->getMicroTime() - $start); } @@ -199,13 +194,13 @@ public function getDbTimeZone(): ?string if (empty($result['time'])) { $tz = null; } else { - $diff = (new DateTime())->diff(new DateTime($result['time'])); + $diff = (new \DateTime())->diff(new \DateTime($result['time'])); // TODO use Carbon $diffInMinutes = ($diff->invert ? -1 : 1) * ($diff->i + 60 * $diff->h); // convert to UTC - $diffInMinutes += intval(floor((new DateTime())->getOffset() / 60)); + $diffInMinutes += intval(floor((new \DateTime())->getOffset() / 60)); // convert in DateInterval - $diff = new DateInterval('PT0S'); + $diff = new \DateInterval('PT0S'); $diff->invert = ($diffInMinutes >= 0) ? 0 : 1; $diff->i = abs($diffInMinutes) % 60; $diff->h = (abs($diffInMinutes) - $diff->i) / 60; @@ -232,12 +227,12 @@ public function getSQLContentBackupMethod(): array // get Tables $tables = $this->loadAll('show tables'); if (!is_array($tables)) { - throw new Exception("Error in '" . __METHOD__ . "' (line " . __LINE__ . ") : 'show tables' sql command did not return an array !"); + throw new \Exception("Error in '" . __METHOD__ . "' (line " . __LINE__ . ") : 'show tables' sql command did not return an array !"); } foreach ($tables as $tableInfo) { if (!is_array($tableInfo)) { - throw new Exception("Error in '" . __METHOD__ . "' (line " . __LINE__ . ") : '\$tableInfo' sql command did not return an array !"); + throw new \Exception("Error in '" . __METHOD__ . "' (line " . __LINE__ . ") : '\$tableInfo' sql command did not return an array !"); } $tableName = array_values($tableInfo)[0]; if (strpos($tableName, $tablesPrefix) === 0) { @@ -367,7 +362,7 @@ public function getSQLContentBackupMethod(): array /*!40101 SET COLLATION_CONNECTION=@OLD_COLLATION_CONNECTION */; SQL; - } catch (Throwable $th) { + } catch (\Throwable $th) { $error = $th->getMessage(); }
f7851fbcce0cfeat(bazar): cache sql, remove false query, use consistant collate
6 files changed · +73 −28
includes/migrations/20240425000000_DropColumnsFromNature.php+4 −4 modified@@ -21,18 +21,18 @@ public function run() // add semantic bazar fields if (!$this->dbService->columnExists('nature', 'bn_sem_context')) { - $this->dbService->query("ALTER TABLE {$this->dbService->prefixTable('nature')} ADD COLUMN bn_sem_context text COLLATE utf8mb4_unicode_ci AFTER bn_condition"); - $this->dbService->query("ALTER TABLE {$this->dbService->prefixTable('nature')} ADD COLUMN bn_sem_type varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL AFTER bn_sem_context"); + $this->dbService->query("ALTER TABLE {$this->dbService->prefixTable('nature')} ADD COLUMN bn_sem_context text COLLATE " . $this->dbService->getCollation() . " AFTER bn_condition"); + $this->dbService->query("ALTER TABLE {$this->dbService->prefixTable('nature')} ADD COLUMN bn_sem_type varchar(255) COLLATE " . $this->dbService->getCollation() . " DEFAULT NULL AFTER bn_sem_context"); $this->dbService->query("ALTER TABLE {$this->dbService->prefixTable('nature')} ADD COLUMN bn_sem_use_template tinyint(1) NOT NULL DEFAULT 1 AFTER bn_sem_type"); } // TODO: What is this??? seems sooo weird $formManager = $this->wiki->services->get(FormManager::class); if (!$formManager->isAvailableOnlyOneEntryOption()) { - $this->dbService->query("ALTER TABLE {$this->dbService->prefixTable('nature')} ADD COLUMN `bn_only_one_entry` enum('Y','N') NOT NULL DEFAULT 'N' COLLATE utf8mb4_unicode_ci;"); + $this->dbService->query("ALTER TABLE {$this->dbService->prefixTable('nature')} ADD COLUMN `bn_only_one_entry` enum('Y','N') NOT NULL DEFAULT 'N' COLLATE " . $this->dbService->getCollation() . ";"); } if (!$formManager->isAvailableOnlyOneEntryMessage()) { - $this->dbService->query("ALTER TABLE {$this->dbService->prefixTable('nature')} ADD COLUMN `bn_only_one_entry_message` text DEFAULT NULL COLLATE utf8mb4_unicode_ci;"); + $this->dbService->query("ALTER TABLE {$this->dbService->prefixTable('nature')} ADD COLUMN `bn_only_one_entry_message` text DEFAULT NULL COLLATE " . $this->dbService->getCollation() . ";"); } } }
includes/services/DbService.php+10 −1 modified@@ -15,6 +15,7 @@ class DbService protected $link; protected $queryLog; protected $collation; + protected $readCache = []; public function __construct(ParameterBagInterface $params) { @@ -99,6 +100,10 @@ public function escape($string) */ public function query($query) { + if (!preg_match('/^\s*SELECT\b/i', $query)) { + $this->readCache = []; + } + if ($this->params->get('debug')) { $start = $this->getMicroTime(); } @@ -146,6 +151,10 @@ public function loadSingle($query): ?array */ public function loadAll($query): array { + if (isset($this->readCache[$query])) { + return $this->readCache[$query]; + } + $data = []; if ($r = $this->query($query)) { while ($row = mysqli_fetch_assoc($r)) { @@ -154,7 +163,7 @@ public function loadAll($query): array mysqli_free_result($r); } - return $data; + return $this->readCache[$query] = $data; } public function count($query): int
includes/services/UserManager.php+19 −18 modified@@ -36,6 +36,7 @@ class UserManager implements UserProviderInterface, PasswordUpgraderInterface protected $tripleStore; private $getOneByNameCacheResults; + private array $associatedEntryCache = []; public const KEY_VOCABULARY = 'http://outils-reseaux.org/_vocabulary/key'; @@ -111,7 +112,7 @@ function ($userAsArray) { public function create($wikiNameOrUser, string $email = '', string $plainPassword = '') { if ($this->securityController->isWikiHibernated()) { - throw new Exception(_t('WIKI_IN_HIBERNATION')); + throw new \Exception(_t('WIKI_IN_HIBERNATION')); } if (is_array($wikiNameOrUser)) { @@ -142,23 +143,23 @@ public function create($wikiNameOrUser, string $email = '', string $plainPasswor 'signuptime' => '', ]; } else { - throw new Exception('First parameter of UserManager->create should be string or array!'); + throw new \Exception('First parameter of UserManager->create should be string or array!'); } if (empty($wikiName)) { - throw new Exception("'Name' parameter of UserManager->create should not be empty!"); + throw new \Exception("'Name' parameter of UserManager->create should not be empty!"); } if (!empty($this->getOneByName($wikiName))) { throw new UserNameAlreadyUsedException(); } if (empty($email)) { - throw new Exception("'email' parameter of UserManager->create should not be empty!"); + throw new \Exception("'email' parameter of UserManager->create should not be empty!"); } if (!empty($this->getOneByEmail($email))) { throw new UserEmailAlreadyUsedException(); } if (empty($plainPassword)) { - throw new Exception("'password' parameter of UserManager->create should not be empty!"); + throw new \Exception("'password' parameter of UserManager->create should not be empty!"); } // clear both the trimmed name and any untrimmed variant stored in cache @@ -248,7 +249,7 @@ public function sendPasswordRecoveryEmail(User $user) public function update(User $user, array $newValues): bool { if ($this->securityController->isWikiHibernated()) { - throw new Exception(_t('WIKI_IN_HIBERNATION')); + throw new \Exception(_t('WIKI_IN_HIBERNATION')); } $newKeys = array_keys($newValues); $authorizedKeys = array_filter($newKeys, function ($key) { @@ -365,16 +366,15 @@ public function getAssociatedEntry($user = '') $user = $this->wiki->services->get(AuthController::class)->getLoggedUser(); if (empty($user['name'])) { return null; - } else { - $user = $user['name']; } + $user = $user['name']; } - if (!empty($GLOBALS['user_entries'][$user])) { - return $GLOBALS['user_entries'][$user]; + if (array_key_exists($user, $this->associatedEntryCache)) { + return $this->associatedEntryCache[$user]; } $vFormManager = $this->wiki->services->get(FormManager::class); - $formsIds = array_keys($vFormManager->getAll()); $vSearchManager = $this->wiki->services->get(SearchManager::class); + $formsIds = array_keys($vFormManager->getAll()); // in case if a username is generated from a bazar entry, nomwiki should be the right id $entry = $vSearchManager->search([ 'queries' => [ @@ -386,15 +386,16 @@ public function getAssociatedEntry($user = '') ], 'formsIds' => $formsIds, ]); - if (empty($entry)) { - return null; + $found = null; + if (!empty($entry)) { + $candidate = array_pop($entry); + if (!empty($candidate['id_fiche'])) { + $found = $candidate; + } } - $found = array_pop($entry); - if (!empty($found['id_fiche'])) { - $GLOBALS['user_entries'][$user] = $found; + $this->associatedEntryCache[$user] = $found; - return $found; - } + return $found; } /* ~~~~~~~~~~~~~~~~~~ implements PasswordUpgraderInterface ~~~~~~~~~~~~~~~~~~ */
includes/YesWikiPerformable.php+22 −5 modified@@ -81,11 +81,28 @@ public function render($templatePath, $data = [], $method = 'render') // add some addition globals $vUserManager = $this->wiki->services->get(UserManager::class); - $userEntry = $vUserManager->getAssociatedEntry(); - $this->twig->addGlobal('user', [ - 'entry' => $userEntry ?? [], - 'name' => (!isset($_SESSION['user']) || empty($_SESSION['user']['name'])) ? '' : $_SESSION['user']['name'], - ]); + $userName = (!isset($_SESSION['user']) || empty($_SESSION['user']['name'])) ? '' : $_SESSION['user']['name']; + $this->twig->addGlobal('user', new class ($vUserManager, $userName) { + public string $name; + private UserManager $userManager; + private bool $entryResolved = false; + private ?array $entry = null; + + public function __construct(UserManager $userManager, string $name) + { + $this->userManager = $userManager; + $this->name = $name; + } + + public function getEntry(): array + { + if (!$this->entryResolved) { + $this->entry = $this->userManager->getAssociatedEntry() ?? []; + $this->entryResolved = true; + } + return $this->entry; + } + }); return $this->twig->$method($templatePath, $data); }
tools/bazar/services/FormManager.php+11 −0 modified@@ -199,6 +199,17 @@ function ($pKey) { ); } + public function getAllIds(): array + { + if ($this->cacheValidatedForAll) { + return array_keys($this->getAll()); + } + + $rows = $this->dbService->loadAll("SELECT bn_id_nature FROM {$this->dbService->prefixTable('nature')}"); + + return array_column($rows, 'bn_id_nature'); + } + public function getMany($formsIds): array { if (count($formsIds) == 0) {
tools/bazar/services/SearchManager.php+7 −0 modified@@ -906,6 +906,10 @@ function ($vFieldName) use ($vKeywordsFields) { $vQueriesConditions = trim($this->buildQueriesConditions($vQueries, $vFields)); + if (str_contains($vQueriesConditions, '((FALSE))')) { + return ''; + } + if ($vQueriesConditions != '') { $vWhereRequest .= ($vWhereRequest != '' ? ' AND ' : '') . $vQueriesConditions; } @@ -1022,6 +1026,9 @@ public function search($params = [], bool $filterOnReadACL = false, bool $useGua $requete = $this->prepareSearchRequest($params, $filterOnReadACL); $searchResults = []; + if ($requete === '') { + return $searchResults; + } $results = $this->dbService->loadAll($requete); $debug = ($this->wiki->GetConfigValue('debug') == 'yes');
08b4e1ff091bfix(bazar): strings for queriesin non-dynamic context
1 file changed · +52 −33
tools/bazar/templates/entries/index/_search_input.twig+52 −33 modified@@ -1,35 +1,54 @@ <div id="bazar-search-{{ listId }}"> - <form action="" method="get" name="search-form" id="search-form" class="form-horizontal"> - {% set handler = url({tag:pageTag})|slice(url({tag:pageTag,handler:' '})|length-1) %} - <input type="hidden" name="wiki" value="{{ pageTag ~ (handler ? '/' ~ handler : '') }}"> - <input type="hidden" name="vue" value="consulter"> - <input type="hidden" name="action" value="recherche"> - <div class="bazar-search control-group"> - <div class="input-group input-prepend input-append"> - <span class="add-on input-group-addon"><i class="fa fa-search icon-search"></i></span> - <input type="text" value="{{ params.keywords }}" name="keywords" placeholder="{{ _t('BAZ_MOT_CLE') }}" class="search-input form-control input-lg"> - {% if forms|length > 1 %} - <span class="input-group-btn search-filter" style="width:auto;max-width:240px;"> - <select onchange="javascript:this.form.submit();" class="form-control input-lg" name="selectedID"> - <option value="">{{ _t('BAZ_TOUS_TYPES_FICHES') }}</option> - {% for form in forms %} - <option value="{{ form.bn_id_nature }}" {% if form.bn_id_nature == selectedID %}selected{% endif %}> - {{ form.bn_label_nature }} - </option> - {% endfor %} - </select> - </span> - {% else %} - <input type="hidden" name="id" value="{{ formId }}"> - {% endif %} - {% if params.query %} - <input type="hidden" name="query" value="{{ params.query|map(p=>p.name ~ p.operator ~ p.value)|join('|')|e('html_attr') }}"> - {% endif %} - <input type="hidden" name="facette" value="{{ facette }}"> - <span class="input-group-btn search-button-container"> - <input value="{{ _t('BAZ_RECHERCHER') }}" class="btn btn-primary" type="submit" /> - </span> - </div> - </div> - </form> + <form action="" + method="get" + name="search-form" + id="search-form" + class="form-horizontal"> + {% set handler = url({tag:pageTag})|slice(url({tag:pageTag,handler:' '})|length-1) %} + <input type="hidden" + name="wiki" + value="{{ pageTag ~ (handler ? '/' ~ handler : '') }}"> + <input type="hidden" name="vue" value="consulter"> + <input type="hidden" name="action" value="recherche"> + <div class="bazar-search control-group"> + <div class="input-group input-prepend input-append"> + <span class="add-on input-group-addon"><i class="fa fa-search icon-search"></i></span> + <input type="text" + value="{{ params.keywords }}" + name="keywords" + placeholder="{{ _t("BAZ_MOT_CLE") }}" + class="search-input form-control input-lg"> + {% if forms|length > 1 %} + <span class="input-group-btn search-filter" + style="width:auto; + max-width:240px"> + <select onchange="javascript:this.form.submit();" + class="form-control input-lg" + name="selectedID"> + <option value="">{{ _t("BAZ_TOUS_TYPES_FICHES") }}</option> + {% for form in forms %} + <option value="{{ form.bn_id_nature }}" + {% if form.bn_id_nature == selectedID %}selected{% endif %}> + {{ form.bn_label_nature }} + </option> + {% endfor %} + </select> + </span> + {% else %} + <input type="hidden" name="id" value="{{ formId }}"> + {% endif %} + {% if params.query %} + <input type="hidden" + name="query" + value="{{ params.query is iterable ? params.query|map(p => p.name ~ p.operator ~ p.value) |join(', ') : params.query }}"> + {% endif %} + <input type="hidden" name="facette" value="{{ facette }}"> + <span class="input-group-btn search-button-container"> + <input value="{{ _t("BAZ_RECHERCHER") }}" + class="btn btn-primary" + type="submit" /> + </span> + </div> + </div> + </form> </div>
Vulnerability mechanics
Root cause
"Unquoted concatenation of user-controlled `bn_id_nature` into an `INSERT` statement in `FormManager::create()` allows SQL injection."
Attack vector
An unauthenticated attacker sends a POST request to `/?BazaR&vue=formulaire` with a crafted `imported-form` parameter. The `bn_id_nature` value is taken directly from the request and concatenated unquoted into the `VALUES` list of an `INSERT` query at `FormManager::create()` [patch_id=1605195]. By embedding arithmetic expressions such as `7330000 + ASCII(SUBSTRING((SELECT ...),1,1))`, the attacker can exfiltrate database contents character-by-character through the observable `bn_id_nature` value returned by the API. No authentication or special configuration is required on a default YesWiki install.
Affected code
The vulnerability resides in `tools/bazar/services/FormManager.php` within the `create()` method (around line 258). The `bn_id_nature` value from user input is concatenated directly into the `INSERT VALUES` list without sanitization or quoting [patch_id=1605195]. The same pattern exists in the update path addressed by [patch_id=1605196].
What the fix does
The patch [patch_id=1605195] wraps the `bn_id_nature` value with `intval()` before concatenation, ensuring only an integer is inserted. The companion patch [patch_id=1605196] applies the same hardening to the `bn_id_nature` handling in the form update path. This closes the injection by preventing any SQL expression from being embedded in the `INSERT` statement, regardless of the attacker's input.
Preconditions
- networkAttacker must be able to send HTTP POST requests to the YesWiki instance
- inputAttacker must supply a crafted imported-form parameter with SQL expression in bn_id_nature
Generated on May 22, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.
References
2News mentions
0No linked articles in our index yet.