{"schema":"threatline.record/2","record":{"id":"GHSA-7m9j-h553-397x","aliases":["GHSA-7m9j-h553-397x"],"summary":"Missing HTTP Signature replay protection allows indefinite re-execution of captured ActivityPub requests","details":"## Summary\r\n\r\n`HttpSignatureService::verifySignature()` validates the cryptographic integrity of HTTP Signature requests and the body digest, but never checks the `Date` header against the current server time. It also maintains no nonce tracking or seen-signature deduplication. A captured valid HTTP Signature request can be replayed at any future time to re-execute the original ActivityPub action on behalf of the original signer, as long as the signer's public key remains at its published URL.\r\n\r\n## Root Cause\r\n\r\nThe `verifySignature()` function includes the `Date` header in the signed data reconstruction (line 144) but only uses it for signature verification, never comparing its value to the current time. The function has no calls to `time()`, `strtotime()`, `DateTime`, or any temporal comparison. No nonce, request-ID deduplication, or seen-signature caching exists anywhere in the `HttpSignatureService` class or the inbox processing pipeline.\r\n\r\n```php\r\nAffected function: HttpSignatureService::verifySignature\r\nFile: tools/bazar/services/HttpSignatureService.php (lines 96-157)\r\n\r\nThe function performs four checks: (1) Signature header presence (line 98),\r\n(2) signature field completeness (line 106), (3) cryptographic signature verification\r\nvia openssl_verify (line 148), and (4) body digest matching (line 152). None of these\r\nchecks include temporal validation. The Date header value flows through signature\r\nreconstruction at line 144 but is never compared to current time.\r\n```\r\n\r\n## Impact\r\n\r\nAn attacker who has access to historical HTTP request headers (through reverse proxy logs, application-level debug logging, WAF/IDS logs, or shared hosting co-tenancy) can capture a valid HTTP Signature request and replay it at any future time. The replay window is unlimited because ActivityPub keys are generated once and persist indefinitely in practice.\r\n\r\nThe attacker can re-execute any previously valid ActivityPub action: a replayed Delete removes an entry, a replayed Update overwrites current content with captured old content, a replayed Follow re-establishes an unwanted subscription, and a replayed Undo reverses a legitimate action. The `assertOwns()` checks added by GHSA-rm6r-grfg-4v78 do not prevent replay because they verify identity (who signed), not temporality (when they signed); a replayed request from a legitimate actor passes all ownership checks.\r\n\r\n## Proof of Concept\r\n\r\n```text\r\nPrerequisites: Access to a valid HTTP POST request with Signature, Date, Digest, and\r\nContent-Type headers sent to the /api/forms/{formId}/actor/inbox endpoint. Capture can\r\noccur from reverse proxy logs, WAF logs, application debug output, or shared hosting\r\nco-tenancy where PHP error logs are accessible.\r\n```\r\n\r\n1. Capture a valid signed request from server logs or shared hosting access:\r\n\r\n```text\r\nPOST /api/forms/2/actor/inbox HTTP/1.1\r\nHost: wiki.example\r\nDate: Mon, 01 Sep 2026 12:00:00 GMT\r\nContent-Type: application/activity+json\r\nDigest: SHA-256=X48E9qOokqqrvdts8nOJRJN3OWDUoyWxBf7kbu9DBPE=\r\nSignature: keyId=\"https://remote.example/actors/alice#main-key\",algorithm=\"rsa-sha256\",headers=\"(request-target) host date digest content-type\",signature=\"base64sig...\"\r\n\r\n{\"type\":\"Delete\",\"actor\":\"https://remote.example/actors/alice\",\"object\":\"https://remote.example/entries/42\"}\r\n```\r\n\r\n2. Replay the identical request days, weeks, or months later using curl or any HTTP client.\r\n\r\n```text\r\nResult: verifySignature() succeeds because the public key is still at the keyId URL,\r\nthe signed data reconstructs identically, and no Date freshness check rejects the\r\nstale request. The Delete action re-executes, removing the entry even if it was\r\nrecreated after the original deletion.\r\n```\r\n\r\n## Attack Chain\r\n\r\n1. **Exposure:** The inbox endpoint `/api/forms/{formId}/actor/inbox` is public (ACL: `{\"public\"}`), accessible to any network party without authentication.\r\n2. **Control:** The attacker controls when to replay the captured request. The request content (headers, body, signature) is the original legitimate content. Timing control enables strategic disruption.\r\n3. **Path:** The replayed POST reaches `ApiController`, which calls `verifySignature()` (HttpSignatureService.php:96), then passes the activity to `processActivity()` (ActivityPubService.php:157).\r\n4. **Guard:** `verifySignature()` checks cryptographic validity (line 148) and digest integrity (line 152) but performs no Date freshness validation. `processActivity()` checks actor binding (line 162) and ownership (lines 196, 210, 224), all of which pass because the request IS from the legitimate actor.\r\n5. **Primitive:** Re-execute any previously valid ActivityPub action: Delete, Update, Create, Follow, or Undo.\r\n6. **Result:** Entry deletion, content reversion to old state, unwanted subscription restoration, or legitimate action reversal. Persistent disruption is possible through repeated replay of captured Delete signatures.\r\n\r\n## Bypass Evidence\r\n\r\nNo prior fix or guard exists for HTTP Signature replay protection in YesWiki. CVE-2026-52767 (GHSA-mv28-wj57-f57g) addressed the `openssl_verify` return-value handling (changing `!openssl_verify(...)` to `openssl_verify(...) !== 1`). GHSA-rm6r-grfg-4v78 addressed cross-actor authorization. Neither addresses temporal validation or replay.\r\n\r\nThe HTTP Signatures specification (draft-cavage-http-signatures-12, Section 2.1.4) explicitly recommends Date freshness validation: \"a receiver SHOULD verify that the Date header is less than a configurable time in the past.\" Mastodon, the dominant ActivityPub implementation, enforces a 12-hour Date freshness window. YesWiki's own `generateSignature()` method emits a Date header (line 46), confirming awareness that Date is part of the protocol, but the verifier never checks it.\r\n\r\nStrongest disproof attempted: \"The content of a replayed request is old valid content, not attacker-chosen new content, so integrity impact is low.\" This was considered but rejected: the attacker controls timing, enabling strategic disruption (replaying a Delete before a critical event); persistent replay of captured Delete signatures creates a denial loop; and within the scope of captured activities, the integrity and availability impact is complete.\r\n\r\n## Affected Versions\r\n\r\n- Ecosystem: Composer (Packagist)\r\n- Package: `yeswiki/yeswiki`\r\n- Confirmed affected range: `>= 4.6.2, <= 4.6.7`\r\n- Latest stable release checked: 4.6.7 (Packagist)\r\n- Fix status: not fixed\r\n\r\nThe `HttpSignatureService::verifySignature()` function was introduced alongside ActivityPub support in v4.6.2 and has never included Date freshness validation in any released version. The prerelease v5.0.0-alpha1 (reorganized to `src/Federation/Service/HttpSignatureService.php`) also lacks replay protection.\r\n\r\n## Suggested Fix\r\n\r\nAdd Date header freshness validation in `verifySignature()`: parse the `Date` header value and reject requests where the date is older than a configurable window (recommended: 30 seconds to 5 minutes, per the HTTP Signatures specification). The check should be performed after signature verification succeeds but before returning the verified actor identity.\r\n\r\nFor defense-in-depth, consider requiring that `digest` be listed in the signed headers (preventing body modification on replay when the remote server omits `digest` from the signature). Activity-ID deduplication (storing processed activity IDs with a TTL) provides an additional layer against replay within the freshness window.\r\n\r\nNo interim mitigation is available without a code change, since the vulnerability is in the signature verification logic itself.\r\n\r\nReported by zx (GitHub: @manus-pi).","categories":["Unclassified"],"product":"yeswiki/yeswiki","vendor":"","ecosystem":null,"affected_ranges":[{"source":"GitHub Advisory Database","source_url":"https://github.com/YesWiki/yeswiki/security/advisories/GHSA-7m9j-h553-397x","source_modified_at":"2026-10-12T10:26:24Z","fetched_at":"2026-10-27T21:14:11.489891Z","package":{"ecosystem":"composer","name":"yeswiki/yeswiki"},"range":">= 4.6.2, < 4.7.0"}],"fixed_versions":[{"source":"GitHub Advisory Database","source_url":"https://github.com/YesWiki/yeswiki/security/advisories/GHSA-7m9j-h553-397x","source_modified_at":"2026-10-12T10:26:24Z","fetched_at":"2026-10-27T21:14:11.489891Z","package":{"ecosystem":"composer","name":"yeswiki/yeswiki"},"version":"4.7.0"}],"cvss":6.5,"severity":"Medium","kev":{"listed":false},"epss":{"probability":null,"percentile":null,"score_date":null},"published_at":"2026-10-12T10:26:24Z","modified_at":"2026-10-12T10:26:24Z","provenance":[{"source":"GitHub Advisory Database","url":"https://github.com/YesWiki/yeswiki/security/advisories/GHSA-7m9j-h553-397x","fetched_at":"2026-10-27T21:14:11.489891Z","source_modified_at":"2026-10-12T10:26:24Z"}],"sources":["GitHub Advisory Database"],"references":[],"locations":[],"motoko_assessment":{"status":"requires_architecture_review","automatic_remediation":false},"classification":{"method":"product-first software-layer rules","rules_version":"2.1","matched_on":null,"confidence":"unknown","reviewed":false},"change_timestamp":"2026-10-12T10:26:24Z","content_hash":"eeabc29c903af7110a13b11820a7edb9b55f72500fab73d9351f8bc7411822be"}}
