yeswiki/yeswiki: Unauthenticated SQL injection through attacker-controlled Bazar field names in public entry-listing APIs
What fixes it
- 4.7.0yeswiki/yeswiki, composerGitHub
composer require "yeswiki/yeswiki:^4.7.0"
A fixed version on one release line is not the upgrade target for every line. Check your exact version and any vendor backports.
Description
Summary
YesWiki 4.6.0 through 4.6.7 and the current doryphore-dev branch contain an unauthenticated SQL injection in the public Bazar entry-listing APIs. An attacker-controlled field name from query (and the same internal field-name path used by searchfields) is incompletely validated and then embedded in both a MySQL JSON path and a backtick-delimited SQL alias. This allows an anonymous attacker to break out of the alias and execute an arbitrary SQL expression in the generated SELECT statement.
The issue was reproduced against the unmodified SearchManager.php shipped by the project. A conditional expression reading yeswiki_users caused a measurable one-second delay when true and no delay when false, establishing a blind SQL oracle over data readable by the YesWiki database account. This oracle also enables unauthenticated administrator account takeover by extracting and reusing a generated password-recovery key.
Details
Public attacker-controlled source
The following public route accepts query and searchfields from the query string without authentication:
// tools/bazar/controllers/ApiController.php
/**
* @Route("/api/forms/{formId}/entries/{output}/{selectedEntries}", methods={"GET"},options={"acl":{"public"}})
*/
public function getAllFormEntries($formId, $output = null, $selectedEntries = null)
{
// ...
$get = $this->getRequest()->query;
$vQuery = $get->get('query') ?? $get->get('queries') ?? null;
// ...
$vSearchFields = $get->has('searchfields')
? urldecode($get->get('searchfields'))
: null;
// ...
$entries = $vBazarListService->getEntries([
'idtypeannonce' => $vFormID,
'queries' => $vQuery,
'keywords' => $vKeywords,
'searchfields' => $vSearchFields,
// ...
]);
}References:
tools/bazar/controllers/ApiController.php:221-277- https://github.com/YesWiki/yeswiki/blob/v4.6.7/tools/bazar/controllers/ApiController.php#L221-L277
BazarListService::getEntries() passes queries and searchfields to SearchManager::search(), which calls SearchManager::prepareSearchRequest() and executes its result through DbService::loadAll().
Incomplete full-path validation
prepareSearchRequest() splits each requested field on . and attempts to traverse the field's declared value structure. The inner condition is duplicated:
// tools/bazar/services/SearchManager.php
$vCurrentArray = $vStructure;
$vFieldFound = true;
foreach ($vJSONPath as $vJSONPathSegment) {
if (is_array($vCurrentArray) && array_key_exists($vJSONPathSegment, $vCurrentArray)) {
if (is_array($vCurrentArray) && array_key_exists($vJSONPathSegment, $vCurrentArray)) {
$vCurrentArray = $vCurrentArray[$vJSONPathSegment];
} else {
$vFieldFound = false;
}
}
}The else is unreachable because it repeats the outer if. If the first segment names a real field but a later segment does not exist, the outer condition is false and the loop simply continues without setting $vFieldFound = false.
For a structured field such as the default map field bf_latitude, an arbitrary second segment therefore survives validation. Because the remaining descriptor does not have a normal _mode_, it is not added to the query WHERE condition, but it is still added to the generated SELECT list.
References:
tools/bazar/services/SearchManager.php:665-773- https://github.com/YesWiki/yeswiki/blob/v4.6.7/tools/bazar/services/SearchManager.php#L665-L773
SQL identifier injection
The complete attacker-controlled field name is then inserted into a JSON path and a backtick-delimited SQL alias:
$vSQLNom = mysqli_real_escape_string($this->wiki->dblink, $vFieldName);
$vRenamedSQLNom = mysqli_real_escape_string(
$this->wiki->dblink,
$this->renameJSONPathVariable($vFieldName)
);
$vSelectRequest[] =
'JSON_UNQUOTE(JSON_EXTRACT(body, \'$.' . $vSQLNom . '\')) AS `'
. $vRenamedSQLNom
. '`';mysqli_real_escape_string() escapes SQL string-literal characters. It does not validate or quote identifiers and does not escape a MySQL backtick. renameJSONPathVariable() only replaces . with __:
protected function renameJSONPathVariable($pPath)
{
return str_replace('.', '__', $pPath);
}An attacker can keep the value valid as a quoted JSON member name while including a backtick that terminates the SQL alias. The remaining bytes become additional expressions in the SELECT list.
References:
tools/bazar/services/SearchManager.php:789-822tools/bazar/services/SearchManager.php:1640-1642- https://github.com/YesWiki/yeswiki/blob/v4.6.7/tools/bazar/services/SearchManager.php#L789-L822
PoC
A default installation contains Bazar form 2 (Agenda) with the structured map field bf_latitude and sample entries. Send the following unauthenticated request to a test instance:
GET /?api/forms/2/entries&query=bf_latitude.%22x%60%2CSLEEP%282%29%20AS%20injected_delay%2Ctag%20AS%20%60y%22%3D%3Dignored HTTP/1.1 Host: TARGET Accept: application/json Connection: close
Adjust the base path if YesWiki is installed below a subdirectory.
The vulnerable response is delayed by approximately two seconds per matching entry because the injected SLEEP(2) expression is evaluated inside the generated SELECT. The equivalent request without the query parameter returns normally:
GET /?api/forms/2/entries HTTP/1.1 Host: TARGET Accept: application/json Connection: close
The payload was validated against the unmodified SearchManager.php from YesWiki v4.6.7. In an isolated one-row MySQL fixture, the control returned in 0.001s and the request above returned in 2.001s.
Impact
This is an unauthenticated SQL injection affecting public Bazar entry-listing functionality.
An anonymous remote attacker can extract arbitrary data readable by the YesWiki database user, including account names, email addresses, password hashes, recovery keys, private wiki metadata, and other application data. Repeated conditional delays can also consume database and PHP worker capacity.
The impact includes deterministic administrator account takeover without cracking a password:
- Extract an administrator name from the
triplesgroup ACL and its email address fromusers. - Submit the public password-recovery form for that address.
UserManager::sendPasswordRecoveryEmail()stores the generated recovery key intriplesbefore sending the email. - Extract the stored
key:issuedAtvalue through the SQL oracle and take the recovery-key portion. - Submit that key with an attacker-chosen password.
LostPasswordAction::resetPassword()accepts it, changes the password, and the recovery flow logs in the affected user.
The demonstrated SQL primitive is read-oriented because YesWiki executes the generated statement through mysqli_query() rather than a multi-statement API. This does not prevent account takeover: the recovery key is a reusable authentication credential, so no stacked query or direct database write is required.
A vulnerable form must expose at least one structured field such as a map/geolocation field. These are normal Bazar field types and are used by map-enabled forms.
Text from GitHub Advisory Database
Affected versions 1
- yeswiki/yeswiki, composer>= 4.6.0, < 4.7.0GitHub