Prevent BOLA (CWE-639) and IDOR in Go REST APIs (Gin / Chi / GORM) by binding SQL queries directly to authenticated tenant context instead of trusting URL parameters.
Imagine checking into Room 204 at a hotel and receiving a valid keycard. But you discover that if you scratch out '204' on the door handle and write '205', the lock clicks open and allows you to rummage through another guest's luggage. The hotel confirmed you were a registered guest (authentication), but never checked whether your key matched Room 205 (object authorization). In BOLA/IDOR, an attacker changes an ID in an API request and accesses another user's private records.
Authentication vs. AuthorizationBOLA (OWASP API #1)Tenancy Query BindingUser logs into the Go API service and receives a valid JWT containing user_id and org_id claims.
User sends GET /api/v1/invoices/:id to fetch an invoice record.
Attacker replaces invoice ID with an ID belonging to a rival organization.
Handler queries db.First(&invoice, "id = ?", paramID) without checking tenant organization ID.
Victim's confidential billing record is returned to the attacker.
// VULNERABLE: Queries solely by client-supplied ID without tenant filter
func GetInvoice(c *gin.Context) {
invoiceID := c.Param("id")
var invoice Invoice
// Vulnerable: trusts client ID without checking authenticated user's organization!
if err := db.First(&invoice, "id = ?", invoiceID).Error; err != nil {
c.JSON(http.StatusNotFound, gin.H{"error": "Invoice not found"})
return
}
c.JSON(http.StatusOK, invoice) // Leaks invoices across tenant boundaries!
}
// HARDENED: Query is strictly scoped to authenticated user's organization ID
func GetInvoice(c *gin.Context) {
invoiceID := c.Param("id")
orgID, exists := c.Get("organization_id")
if !exists {
c.JSON(http.StatusUnauthorized, gin.H{"error": "Unauthorized"})
return
}
var invoice Invoice
// Defense-in-depth: query filters by BOTH invoice ID and authenticated org ID
if err := db.Where("id = ? AND organization_id = ?", invoiceID, orgID).First(&invoice).Error; err != nil {
// Return 404 rather than 403 to prevent ID enumeration
c.JSON(http.StatusNotFound, gin.H{"error": "Invoice not found"})
return
}
c.JSON(http.StatusOK, invoice)
}