{"schema":"threatline.record/2","record":{"id":"GHSA-hgxv-w4r8-8877","aliases":["GHSA-hgxv-w4r8-8877"],"summary":"Unauthenticated SQL injection through attacker-controlled Bazar field names in public entry-listing APIs","details":"### Summary\r\n\r\nYesWiki 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.\r\n\r\nThe 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.\r\n\r\n### Details\r\n\r\n#### Public attacker-controlled source\r\n\r\nThe following public route accepts `query` and `searchfields` from the query string without authentication:\r\n\r\n```php\r\n// tools/bazar/controllers/ApiController.php\r\n/**\r\n * @Route(\"/api/forms/{formId}/entries/{output}/{selectedEntries}\", methods={\"GET\"},options={\"acl\":{\"public\"}})\r\n */\r\npublic function getAllFormEntries($formId, $output = null, $selectedEntries = null)\r\n{\r\n    // ...\r\n    $get = $this->getRequest()->query;\r\n    $vQuery = $get->get('query') ?? $get->get('queries') ?? null;\r\n    // ...\r\n    $vSearchFields = $get->has('searchfields')\r\n        ? urldecode($get->get('searchfields'))\r\n        : null;\r\n    // ...\r\n    $entries = $vBazarListService->getEntries([\r\n        'idtypeannonce' => $vFormID,\r\n        'queries' => $vQuery,\r\n        'keywords' => $vKeywords,\r\n        'searchfields' => $vSearchFields,\r\n        // ...\r\n    ]);\r\n}\r\n```\r\n\r\nReferences:\r\n\r\n- `tools/bazar/controllers/ApiController.php:221-277`\r\n- https://github.com/YesWiki/yeswiki/blob/v4.6.7/tools/bazar/controllers/ApiController.php#L221-L277\r\n\r\n`BazarListService::getEntries()` passes `queries` and `searchfields` to `SearchManager::search()`, which calls `SearchManager::prepareSearchRequest()` and executes its result through `DbService::loadAll()`.\r\n\r\n#### Incomplete full-path validation\r\n\r\n`prepareSearchRequest()` splits each requested field on `.` and attempts to traverse the field's declared value structure. The inner condition is duplicated:\r\n\r\n```php\r\n// tools/bazar/services/SearchManager.php\r\n$vCurrentArray = $vStructure;\r\n$vFieldFound = true;\r\n\r\nforeach ($vJSONPath as $vJSONPathSegment) {\r\n    if (is_array($vCurrentArray) && array_key_exists($vJSONPathSegment, $vCurrentArray)) {\r\n        if (is_array($vCurrentArray) && array_key_exists($vJSONPathSegment, $vCurrentArray)) {\r\n            $vCurrentArray = $vCurrentArray[$vJSONPathSegment];\r\n        } else {\r\n            $vFieldFound = false;\r\n        }\r\n    }\r\n}\r\n```\r\n\r\nThe `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`.\r\n\r\nFor 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.\r\n\r\nReferences:\r\n\r\n- `tools/bazar/services/SearchManager.php:665-773`\r\n- https://github.com/YesWiki/yeswiki/blob/v4.6.7/tools/bazar/services/SearchManager.php#L665-L773\r\n\r\n#### SQL identifier injection\r\n\r\nThe complete attacker-controlled field name is then inserted into a JSON path and a backtick-delimited SQL alias:\r\n\r\n```php\r\n$vSQLNom = mysqli_real_escape_string($this->wiki->dblink, $vFieldName);\r\n$vRenamedSQLNom = mysqli_real_escape_string(\r\n    $this->wiki->dblink,\r\n    $this->renameJSONPathVariable($vFieldName)\r\n);\r\n\r\n$vSelectRequest[] =\r\n    'JSON_UNQUOTE(JSON_EXTRACT(body, \\'$.' . $vSQLNom . '\\')) AS `'\r\n    . $vRenamedSQLNom\r\n    . '`';\r\n```\r\n\r\n`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 `__`:\r\n\r\n```php\r\nprotected function renameJSONPathVariable($pPath)\r\n{\r\n    return str_replace('.', '__', $pPath);\r\n}\r\n```\r\n\r\nAn 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.\r\n\r\nReferences:\r\n\r\n- `tools/bazar/services/SearchManager.php:789-822`\r\n- `tools/bazar/services/SearchManager.php:1640-1642`\r\n- https://github.com/YesWiki/yeswiki/blob/v4.6.7/tools/bazar/services/SearchManager.php#L789-L822\r\n\r\n### PoC\r\n\r\nA 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:\r\n\r\n```http\r\nGET /?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\r\nHost: TARGET\r\nAccept: application/json\r\nConnection: close\r\n```\r\n\r\nAdjust the base path if YesWiki is installed below a subdirectory.\r\n\r\nThe 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:\r\n\r\n```http\r\nGET /?api/forms/2/entries HTTP/1.1\r\nHost: TARGET\r\nAccept: application/json\r\nConnection: close\r\n```\r\n\r\nThe 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`.\r\n\r\n### Impact\r\n\r\nThis is an unauthenticated SQL injection affecting public Bazar entry-listing functionality.\r\n\r\nAn 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.\r\n\r\nThe impact includes deterministic administrator account takeover without cracking a password:\r\n\r\n1. Extract an administrator name from the `triples` group ACL and its email address from `users`.\r\n2. Submit the public password-recovery form for that address. `UserManager::sendPasswordRecoveryEmail()` stores the generated recovery key in `triples` before sending the email.\r\n3. Extract the stored `key:issuedAt` value through the SQL oracle and take the recovery-key portion.\r\n4. Submit that key with an attacker-chosen password. `LostPasswordAction::resetPassword()` accepts it, changes the password, and the recovery flow logs in the affected user.\r\n\r\nThe 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.\r\n\r\nA 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.","categories":["Unclassified"],"product":"yeswiki/yeswiki","vendor":"","ecosystem":null,"affected_ranges":[{"source":"GitHub Advisory Database","source_url":"https://github.com/YesWiki/yeswiki/security/advisories/GHSA-hgxv-w4r8-8877","source_modified_at":"2026-10-09T13:38:12Z","fetched_at":"2026-10-25T21:40:15.971031Z","package":{"ecosystem":"composer","name":"yeswiki/yeswiki"},"range":">= 4.6.0, < 4.7.0"}],"fixed_versions":[{"source":"GitHub Advisory Database","source_url":"https://github.com/YesWiki/yeswiki/security/advisories/GHSA-hgxv-w4r8-8877","source_modified_at":"2026-10-09T13:38:12Z","fetched_at":"2026-10-25T21:40:15.971031Z","package":{"ecosystem":"composer","name":"yeswiki/yeswiki"},"version":"4.7.0"}],"cvss":9.4,"severity":"Critical","kev":{"listed":false},"epss":{"probability":null,"percentile":null,"score_date":null},"published_at":"2026-10-09T13:38:12Z","modified_at":"2026-10-09T13:38:12Z","provenance":[{"source":"GitHub Advisory Database","url":"https://github.com/YesWiki/yeswiki/security/advisories/GHSA-hgxv-w4r8-8877","fetched_at":"2026-10-25T21:40:15.971031Z","source_modified_at":"2026-10-09T13:38:12Z"}],"sources":["GitHub Advisory Database"],"references":[],"locations":[],"motoko_assessment":{"status":"requires_architecture_review","automatic_remediation":false},"classification":{"method":"product-first software-layer rules","rules_version":"2.1","matched_on":null,"confidence":"unknown","reviewed":false},"change_timestamp":"2026-10-09T13:38:12Z","content_hash":"d6a173bb20f5326c0b7047bedeb7f2fcad76822de901197f37afaa28a40e932d"}}
