All vulnerabilities
GHSA-hgxv-w4r8-8877

yeswiki/yeswiki: Unauthenticated SQL injection through attacker-controlled Bazar field names in public entry-listing APIs

yeswiki/yeswiki
CVSS
9.4Critical
ExploitationNo known exploitationNot in CISA KEV, which does not prove it is unexploitedCISA KEV
Exploit odds (EPSS)No scoreNo FIRST EPSS score in this snapshot yet
Fixed in4.7.0Fixed versionGitHub
Published9 Oct 2026Updated 9 Oct 2026GitHub

What fixes it

  • 4.7.0yeswiki/yeswiki, composerGitHubcomposer 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-822
  • tools/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:

  1. Extract an administrator name from the triples group ACL and its email address from users.
  2. Submit the public password-recovery form for that address. UserManager::sendPasswordRecoveryEmail() stores the generated recovery key in triples before sending the email.
  3. Extract the stored key:issuedAt value through the SQL oracle and take the recovery-key portion.
  4. 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