yeswiki/yeswiki: Unauthenticated SQL injection in YesWiki Bazar entries search API via unescaped backtick in query field name
What fixes it
- 4.7.0yeswiki/yeswiki, composerGitHub
composer require "yeswiki/yeswiki:^4.7.0" - 5.0.0-alpha4yeswiki/yeswiki, composerGitHub
composer require "yeswiki/yeswiki:^5.0.0-alpha4"
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 exposes a public, unauthenticated Bazar entries search API at GET /api/forms/{formId}/entries/{output}/{selectedEntries} (and the sibling /api/entries/bazarlist route). The query and searchfields request parameters let a caller name the entry fields to search. An attacker-supplied field name flows unmodified into a SQL SELECT clause where it is placed inside a backtick-quoted column alias. The only escaping applied is mysqli_real_escape_string, which does not escape the backtick character, so a field name containing a backtick closes the identifier context and injects attacker-controlled SQL. A single unauthenticated GET request achieves arbitrary SQL SELECT execution, allowing full read of the database (including user credentials) and denial of service.
Root Cause
The search pipeline resolves each requested field name against the target form's field definitions. A field name that matches no known property is not rejected; it is retained with a MISSING_PROPERTY descriptor and still proceeds to SQL construction. During construction the field name is embedded into a backtick-quoted SQL alias, escaped only with mysqli_real_escape_string. That function is designed for string-literal contexts and escapes NUL, newline, carriage return, backslash, single quote, double quote, and Ctrl+Z; it does not escape the backtick, which is MySQL's identifier-quoting character. Because the value is used as an identifier (an alias) rather than a string literal, the escaping is contextually wrong and a backtick in the field name breaks out of the alias.
tools/bazar/services/SearchManager.php (line 809-812) $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 . '`'; Vulnerable function: SearchManager::prepareSearchRequest() Public entry route: tools/bazar/controllers/ApiController.php (line 221)
Impact
An unauthenticated network attacker who can reach the site and knows or guesses a Bazar form identifier (Bazar is a core YesWiki feature present in typical deployments) can execute arbitrary SQL SELECT statements in the application's database context. Scalar subqueries injected as additional SELECT columns can read any table the database user can access; because the API serializes result columns into its JSON response, extracted data (for example username and password-hash pairs from the users table) is returned directly, with no blind technique required. Injecting a SLEEP() subquery blocks database worker threads; concurrent unauthenticated requests exhaust the connection pool and render the site unavailable. Execution is limited to a single statement (the database driver rejects stacked queries), so direct INSERT, UPDATE, or DELETE through this vector is not demonstrated; the impact is confidentiality and availability, not direct integrity modification.
Proof of Concept
Preconditions: a reachable YesWiki instance (>= 4.6.7) with at least one Bazar form. No authentication, cookies, or tokens are required. Form id 1 is used for illustration.
Data exfiltration (reads username and password-hash pairs from the users table into the JSON response):
GET /api/forms/1/entries?query=x%60%2C(SELECT+GROUP_CONCAT(name%2C0x3a%2Cpassword)+FROM+yeswiki_users)+as+%60d!=x HTTP/1.1 Host: target
The query value decodes to the field name x,(SELECT GROUP_CONCAT(name,0x3a,password) FROM yeswiki_users) as d followed by the !=x operator and comparison value. yeswiki_ is the default table prefix; adjust it for a customized install.
Denial of service (blocks a database thread for 30 seconds per request):
GET /api/forms/1/entries?query=x%60%2C(SELECT+SLEEP(30))+as+%60d!=x HTTP/1.1 Host: target
Resulting SQL fragment produced in the generated SELECT list: JSON_UNQUOTE(JSON_EXTRACT(body, '$.x`,(SELECT SLEEP(30)) as `d')) AS `x`,(SELECT SLEEP(30)) as `d` MySQL parses this as two columns: an alias `x` and an independent scalar subquery (SELECT SLEEP(30)) aliased `d`, which executes for every evaluated row.
Attack Chain
- Exposure: The route
GET /api/forms/{formId}/entries/{output}/{selectedEntries}is annotatedoptions={"acl":{"public"}}(tools/bazar/controllers/ApiController.php line 221), so it is reachable with no authentication. The/api/entries/bazarlistroute is likewise public. - Control: The handler reads the
queryparameter (and, equivalently,searchfields) directly from the request.aggregateQueriesonly splits on|and deduplicates; no character filtering is applied, so the attacker fully controls the field-name text. - Path: The value reaches
SearchManager::prepareSearchRequest(), whereparseQuery()extracts the field name with the regex character class[^=!<>]*. Only=,!,<, and>are excluded, so backticks, parentheses, commas, and spaces survive. An unknown field is taggedMISSING_PROPERTYbut is still added to the fields collection and reaches SQL construction. - Guard: The only neutralization is
mysqli_real_escape_stringapplied to the field name before it is placed in a backtick-quoted alias. That function does not escape the backtick, and no identifier whitelist, allowed-character check, or authorization gate exists on the path.renameJSONPathVariable()only replaces dots, which the payload avoids. - Primitive: The backtick in the field name closes the alias identifier, and the following text is parsed by MySQL as additional SELECT columns, including an attacker-chosen scalar subquery. The
!=operator against aMISSING_PROPERTYfield yields an unconditionalTRUEin the WHERE clause, so rows are evaluated and the injected subquery runs. - Result: A single unauthenticated request yields arbitrary SQL SELECT execution: subqueries read arbitrary tables (credentials, content, configuration) and return the data in the JSON response, and
SLEEP()subqueries exhaust database threads for denial of service.
Bypass Evidence
No effective guard exists on this path, and no prior fix addresses it. The escaping in place, mysqli_real_escape_string, is the wrong primitive for an identifier context: it escapes string-literal metacharacters but leaves the backtick untouched, which is exactly the character needed to break out of a backtick-quoted alias. The following disproof attempts were made and each failed to block the chain:
- Searched the Bazar tools for request-filtering middleware, kernel-request subscribers, or field-name validation and found none; the public ACL annotation is authoritative.
- Traced the full data flow from the raw request parameter to SQL construction; no whitelist or sanitization is applied at any step.
aggregateQueriesonly splits and deduplicates. - Confirmed the
parseQueryregex[^=!<>]*excludes only four characters, so backtick, parentheses, comma, and space pass through. - Confirmed unknown field names are not skipped: they receive
MISSING_PROPERTYyet are still added to the field collection and built into the SELECT clause (the extraction flag is false, so the alias SQL is emitted). - Considered whether MySQL would reject the statement: the invalid JSON path inside the string literal returns NULL rather than a parse error, and the injected
(SELECT ...)is a grammatically independent column that executes. - Verified the driver uses single-statement execution, which correctly prevents stacked-query INSERT/UPDATE/DELETE, so the assessed impact is confined to SELECT-context reads and availability.
Affected Versions
Ecosystem: composerPackage: yeswiki/yeswikiConfirmed affected range: >= 4.6.7, <= 5.0.0-alpha3Latest release checked: 4.6.7 (Packagist), 5.0.0-alpha3 (Packagist prerelease)Fix status: not fixed in any released version or on the current development branch
The backtick-quoted SELECT alias sink was present from 4.6.0, but the demonstrated end-to-end chain depends on the search building an outer query that returns rows so the injected column value is serialized into the API response. In releases 4.6.0 through 4.6.6 a requested field that matches no form property produces an unconditional false search condition (and, from 4.6.4, the request is emptied entirely), so the described payload returns no rows and the injected data does not surface. Starting in 4.6.7 the missing-property branch produces a true condition for the != operator, so all rows are returned and the injected subquery result is serialized directly into the JSON response; this behavior is unchanged on the current development branch and in the 5.0.0-alpha1 through 5.0.0-alpha3 prereleases (where the search code is relocated to src/Search/Service/SearchManager.php with an unquoted alias but the same reachable public route and missing-property handling). Releases 4.5.5 and earlier ship a different, much smaller search implementation without prepareSearchRequest() or the alias pattern and are not affected. No single upper bound is given because the confirmed set spans the stable 4.6.7 release and the 5.0.0-alpha prerelease line; earlier 4.6.x releases were reviewed and are not confirmed affected for this chain.
Suggested Fix
Field names used to build SQL identifiers must be validated as identifiers, not escaped as string literals. Enforce a strict allowlist (for example, restrict field names to [A-Za-z0-9_.]+) and reject or drop any requested field that does not match a known form property, so that unknown MISSING_PROPERTY names never reach SQL construction. If arbitrary identifiers must be supported, escape them for identifier context by doubling embedded backticks before placing them inside a backtick-quoted alias. Apply the same validation to every caller and every parameter that feeds field names into the search pipeline (both query and searchfields, and both the forms-entries and bazarlist routes). As an interim mitigation, restrict network access to the public Bazar entries API endpoints until the input is validated.
Reported by zx (GitHub: @manus-pi).
Text from GitHub Advisory Database
Affected versions 2
- yeswiki/yeswiki, composer>= 4.6.7, < 4.7.0GitHub
- yeswiki/yeswiki, composer>= 5.0.0-alpha1, < 5.0.0-alpha4GitHub