Overview
CVE-2026-31805 describes an authorization bypass in Discourse's poll plugin where an authenticated user could vote on, remove votes from, or toggle the status of polls they should not access by sending post_id[] parameters that map to different posts. The vulnerability exploited an object-level mismatch between the resource lookup and the authorization check, allowing access to a poll associated with a different post. In a Go (Gin) context, this pattern translates to code that uses an input array to identify a resource for lookup while performing authorization against a distinct resource derived from that input, enabling cross-resource actions. The impact is serious: attackers can alter poll outcomes or statuses without proper ownership checks, eroding trust and poll integrity. CVE-2026-31805 is associated with CWE-20 (Improper Input Validation) and CWE-863 (Incorrect Authorization).
Affected Versions
Prior to 2026.3.0-latest.1, 2026.2.1, and 2026.1.2 (Discourse Poll plugin cited by CVE-2026-31805). In a Go Gin context, the vulnerable pattern mirrors those versions’ risk.
Code Fix Example
Go (Gin) API Security Remediation
/* Vulnerable pattern vs fixed pattern in Go (Gin) */
package main
import (
"net/http"
"strconv"
"github.com/gin-gonic/gin"
)
type Poll struct { ID int64; PostID int64; OwnerID int64 }
type User struct { ID int64 }
func main() {
r := gin.Default()
r.POST("/poll/vote", VotePollVulnerable)
r.POST("/poll/vote_fixed", VotePollFixed)
r.Run()
}
func getCurrentUser(c *gin.Context) *User { return &User{ID: 1} }
var polls = map[int64]*Poll{
1: {ID: 1, PostID: 101, OwnerID: 1},
2: {ID: 2, PostID: 202, OwnerID: 2},
}
func GetPollByPostID(postID int64) *Poll {
for _, p := range polls {
if p.PostID == postID {
return p
}
}
return nil
}
func canAccess(user *User, postID int64) bool {
for _, p := range polls {
if p.PostID == postID && p.OwnerID == user.ID {
return true
}
}
return false
}
// Vulnerable: reads post_id[] and uses a lookup based on it, but performs authorization against poll.PostID (a different resource)
func VotePollVulnerable(c *gin.Context) {
user := getCurrentUser(c)
c.Request.ParseForm()
postIDs := c.Request.Form["post_id[]"]
if len(postIDs) == 0 {
c.Status(http.StatusBadRequest)
return
}
pid, _ := strconv.ParseInt(postIDs[0], 10, 64)
poll := GetPollByPostID(pid)
if poll == nil {
c.Status(http.StatusNotFound)
return
}
// BAD: authorization check may apply to poll.PostID instead of the target resource
if !canAccess(user, poll.PostID) {
c.Status(http.StatusForbidden)
return
}
c.Status(http.StatusOK)
}
// Fixed: authorize against the actual target resource derived from the request
func VotePollFixed(c *gin.Context) {
user := getCurrentUser(c)
pidStr := c.PostForm("post_id")
pid, _ := strconv.ParseInt(pidStr, 10, 64)
poll := GetPollByPostID(pid)
if poll == nil {
c.Status(http.StatusNotFound)
return
}
// Enforce ownership-based access on the actual resource
if poll.OwnerID != user.ID {
c.Status(http.StatusForbidden)
return
}
c.Status(http.StatusOK)
}