Bola Idor in Gin
How BOLA/IdOR Manifests in Gin
BOLA (Broken Object Level Authorization) and IDOR (Insecure Direct Object Reference) are essentially the same vulnerability — they occur when an API endpoint uses user-controlled input (like an ID in the URL) to access resources without verifying that the authenticated user has permission to access that specific resource. In Gin applications, this manifests in several common patterns.
The most frequent scenario involves route handlers that accept resource identifiers directly from the URL path. Consider this Gin handler:
func getUser(c *gin.Context) {
id := c.Param("id")
user, err := db.GetUserByID(id)
if err != nil {
c.JSON(404, gin.H{"error": "not found"})
return
}
c.JSON(200, user)
}
The route might be registered as /users/:id. The vulnerability here is that the handler retrieves any user by ID without checking whether the requesting user is authorized to view that specific user's data. An attacker can simply iterate through user IDs to access other users' information.
Another common pattern appears in nested resource endpoints:
func getDocument(c *gin.Context) {
userID := c.Param("user_id")
docID := c.Param("doc_id")
doc, err := db.GetDocument(docID)
if err != nil {
c.JSON(404, gin.H{"error": "not found"})
return
}
c.JSON(200, doc)
}
Even though the URL includes user_id, the handler doesn't verify that the document belongs to that user. An authenticated user could access any document by knowing or guessing its ID.
Query parameter-based BOLA is equally problematic:
func getOrders(c *gin.Context) {
userID := c.Query("user_id")
orders, err := db.GetOrdersByUserID(userID)
if err != nil {
c.JSON(500, gin.H{"error": "internal error"})
return
}
c.JSON(200, orders)
}
Here, an attacker can modify the user_id query parameter to view other users' orders. The handler trusts the client-provided value without any authorization check.
Batch operations introduce additional BOLA risks:
func deleteUsers(c *gin.Context) {
var req struct {
UserIDs []string `json:"user_ids"`
}
if err := c.BindJSON(&req); err != nil {
c.JSON(400, gin.H{"error": "invalid request"})
return
}
for _, id := range req.UserIDs {
db.DeleteUser(id)
}
c.JSON(200, gin.H{"status": "success"})
}
This endpoint allows deleting multiple users in one request, but without verifying that the requester has permission to delete each specified user.
Gin's middleware system can help mitigate these issues, but only if used correctly. A common mistake is implementing authorization middleware that only checks authentication status, not resource-specific permissions:
func AuthMiddleware() gin.HandlerFunc {
return func(c *gin.Context) {
token := c.GetHeader("Authorization")
if token == "" {
c.JSON(401, gin.H{"error": "unauthorized"})
c.Abort()
return
}
// Missing: verify token belongs to user making request
c.Next()
}
}
Without proper permission checks in each handler, this middleware provides a false sense of security.
Gin-Specific Detection
Detecting BOLA/IdOR in Gin applications requires both static code analysis and dynamic testing. For dynamic testing, middleBrick's black-box scanning approach is particularly effective because it doesn't require access to source code or credentials.
When scanning a Gin API endpoint, middleBrick tests for BOLA by attempting authenticated requests with modified resource identifiers. For example, if you scan https://api.example.com/users/123, middleBrick will:
- Authenticate using the provided credentials or session
- Capture the user ID from the authenticated response
- Attempt requests with modified IDs (e.g.,
/users/124,/users/999) - Analyze responses for information disclosure or unauthorized access
The scanner looks for specific response patterns that indicate BOLA, such as:
- HTTP 200 responses with data that shouldn't be accessible
- HTTP 404 responses that differ in content between valid and invalid IDs (indicating the resource exists but is being hidden)
- Timing differences between authorized and unauthorized requests
For Gin applications specifically, middleBrick's OpenAPI analysis can identify potential BOLA vulnerabilities by examining route definitions and parameter usage. The scanner resolves $ref references to understand the complete API structure and identifies endpoints that:
- Use path parameters for resource identification
- Accept user-controlled identifiers without apparent validation
- Implement CRUD operations on user-owned resources
Static analysis of Gin code can reveal BOLA patterns through code review. Look for:
// Vulnerable pattern - no authorization check
func getAccount(c *gin.Context) {
id := c.Param("id")
account, err := db.GetAccount(id)
if err != nil {
c.JSON(404, gin.H{"error": "not found"})
return
}
c.JSON(200, account)
}
Tools like go vet and static analysis frameworks can flag suspicious patterns, though they won't catch all BOLA cases since authorization logic is often application-specific.
During development, you can use middleware to log and monitor authorization checks:
func AuthorizationMiddleware() gin.HandlerFunc {
return func(c *gin.Context) {
// Extract user from context
user, exists := c.Get("user")
if !exists {
c.JSON(401, gin.H{"error": "unauthorized"})
c.Abort()
return
}
// Check resource ownership
resourceID := c.Param("id")
if !user.HasAccessTo(resourceID) {
c.JSON(403, gin.H{"error": "forbidden"})
c.Abort()
return
}
c.Next()
}
}
This middleware should be applied to all routes that access user-specific resources.
Gin-Specific Remediation
Remediating BOLA/IdOR in Gin applications requires implementing proper authorization checks at the handler level. The most effective approach is to verify resource ownership before processing any request that uses user-controlled identifiers.
Here's a secure implementation for the user retrieval endpoint:
func getUser(c *gin.Context) {
id := c.Param("id")
// Get authenticated user from context
currentUser, exists := c.Get("user")
if !exists {
c.JSON(401, gin.H{"error": "unauthorized"})
return
}
// Verify the requested user matches the authenticated user
if currentUser.ID != id {
c.JSON(403, gin.H{"error": "forbidden"})
return
}
user, err := db.GetUserByID(id)
if err != nil {
c.JSON(404, gin.H{"error": "not found"})
return
}
c.JSON(200, user)
}
This pattern ensures users can only access their own data. For admin users who should have broader access, you'd add role-based checks:
func getUser(c *gin.Context) {
id := c.Param("id")
currentUser := c.MustGet("user").(*User)
// Admins can access any user
if currentUser.Role != "admin" && currentUser.ID != id {
c.JSON(403, gin.H{"error": "forbidden"})
return
}
user, err := db.GetUserByID(id)
if err != nil {
c.JSON(404, gin.H{"error": "not found"})
return
}
c.JSON(200, user)
}
For nested resources, implement ownership verification at each level:
func getDocument(c *gin.Context) {
userID := c.Param("user_id")
docID := c.Param("doc_id")
currentUser := c.MustGet("user").(*User)
// Verify document belongs to user
doc, err := db.GetDocument(docID)
if err != nil {
c.JSON(404, gin.H{"error": "not found"})
return
}
if doc.UserID != userID || (currentUser.Role != "admin" && currentUser.ID != userID) {
c.JSON(403, gin.H{"error": "forbidden"})
return
}
c.JSON(200, doc)
}
Create reusable authorization middleware for common patterns:
func ResourceOwnerMiddleware(resourceIDParam string) gin.HandlerFunc {
return func(c *gin.Context) {
currentUser := c.MustGet("user").(*User)
resourceID := c.Param(resourceIDParam)
resource, err := db.GetResource(resourceID)
if err != nil {
c.JSON(404, gin.H{"error": "not found"})
c.Abort()
return
}
if resource.OwnerID != currentUser.ID && currentUser.Role != "admin" {
c.JSON(403, gin.H{"error": "forbidden"})
c.Abort()
return
}
c.Set("resource", resource)
c.Next()
}
}
Apply this middleware to routes:
r := gin.New()
r.Use(authMiddleware)
r.GET("/resources/:id", ResourceOwnerMiddleware("id"), handleResource)
For batch operations, validate each item individually:
func deleteUsers(c *gin.Context) {
var req struct {
UserIDs []string `json:"user_ids"`
}
if err := c.BindJSON(&req); err != nil {
c.JSON(400, gin.H{"error": "invalid request"})
return
}
currentUser := c.MustGet("user").(*User)
results := make([]map[string]interface{}, 0, len(req.UserIDs))
for _, id := range req.UserIDs {
if currentUser.Role != "admin" && currentUser.ID != id {
results = append(results, map[string]interface{}{
"user_id": id,
"status": "forbidden",
})
continue
}
err := db.DeleteUser(id)
if err != nil {
results = append(results, map[string]interface{}{
"user_id": id,
"status": "error",
"error": err.Error(),
})
} else {
results = append(results, map[string]interface{}{
"user_id": id,
"status": "deleted",
})
}
}
c.JSON(200, gin.H{"results": results})
}
Implement database-level constraints where possible to provide defense in depth:
// Database query with ownership check
func getUserByIDAndOwner(ctx context.Context, id string, ownerID string) (*User, error) {
var user User
err := db.QueryRow(ctx,
"SELECT * FROM users WHERE id = $1 AND (owner_id = $2 OR role = 'admin')",
id, ownerID,
).Scan(&user)
return &user, err
}
This approach ensures that even if application-level authorization fails, the database will prevent unauthorized access.
Related CWEs: bolaAuthorization
| CWE ID | Name | Severity |
|---|---|---|
| CWE-250 | Execution with Unnecessary Privileges | HIGH |
| CWE-639 | Insecure Direct Object Reference | CRITICAL |
| CWE-732 | Incorrect Permission Assignment | HIGH |