flawopen.com/bola-idor/Go

● CWE-639 · 緊急
セキュリティ研究 · FlawOpen

脆弱性の解説:Broken Object Level Authorization (BOLA / IDOR) in Go

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.

💡 わかりやすい解説 (ELI5)

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. Authorization
Authentication verifies identity. Authorization checks whether that identity owns or has permission for the specific resource.
BOLA (OWASP API #1)
Failure to validate object ownership, enabling attackers to access other tenants' data by altering request parameters.
Tenancy Query Binding
Passing claims from the request context into the SQL WHERE clause (db.Where("id = ? AND org_id = ?", id, orgID)).

ステップ・バイ・ステップの攻撃フロー

Step 1

Token Issuance

User logs into the Go API service and receives a valid JWT containing user_id and org_id claims.

Step 2

Route Invocation

User sends GET /api/v1/invoices/:id to fetch an invoice record.

Step 3

ID Manipulation

Attacker replaces invoice ID with an ID belonging to a rival organization.

Step 4

Unscoped DB Query

Handler queries db.First(&invoice, "id = ?", paramID) without checking tenant organization ID.

Step 5

Data Leakage

Victim's confidential billing record is returned to the attacker.

ソースコード比較:脆弱 vs 堅牢化

✕ 脆弱な実装
// 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)
}

エンジニアリング&システム堅牢化チェックリスト

References