Insecure Direct Object Reference in Fiber with Basic Auth
Insecure Direct Object Reference in Fiber with Basic Auth — how this specific combination creates or exposes the vulnerability
An Insecure Direct Object Reference (BOLA/IDOR) occurs when an API exposes internal object references such as sequential IDs in URLs or parameters without proper authorization checks. When this pattern is combined with HTTP Basic Authentication in the Fiber web framework for Go, the risk is compounded because authentication and authorization are not automatically coupled. Basic Auth provides a simple mechanism to send a username and password in each request, but it does not enforce that a given user can only access their own resources. If a Fiber route uses a user ID or record ID directly from the URL—e.g., /users/123/profile—and only validates that the credentials are valid without confirming the authenticated user matches the requested ID, the endpoint becomes vulnerable.
Consider a typical Fiber handler that retrieves a user profile by ID from a database. The code might decode the Basic Auth credentials, extract the username, and then query the database using a numeric ID from the route parameters. If the developer does not scope the database query to the authenticated user, an attacker who knows or guesses another user’s ID can retrieve or modify that user’s data. This is a classic IDOR because the object reference (the numeric user ID) is direct and lacks ownership validation. In a black-box scan, middleBrick would flag this as a BOLA/IDOR finding because the endpoint trusts user-supplied identifiers without ensuring the authenticated subject has the right to access that identifier.
Even when Basic Auth is correctly implemented to verify credentials, the application must still enforce access controls at the business logic layer. For example, decoding the Base64-encoded header and extracting the username is straightforward in Fiber, but that username must be used to scope every subsequent query. Without this scoping, the API surface includes endpoints that are effectively public for ID manipulation, despite being protected by Basic Auth. middleBrick tests this by sending authenticated requests as one user to another user’s resource and checking whether the response contains data or a 200 status when it should be 403 or 404. The presence of Basic Auth headers does not automatically prevent IDOR; it only provides identity, not authorization.
Real-world attack patterns often chain IDOR with other weaknesses. An attacker might use a stolen or leaked Basic Auth credential list to iterate over predictable IDs, automating access to sensitive records such as financial details or personal information. This aligns with the OWASP API Top 10 category for Broken Object Level Authorization and can map to compliance frameworks like PCI-DSS and SOC2, where access control and data segregation are required. Because middleBrick scans unauthenticated attack surfaces using raw requests, it can detect scenarios where authentication headers are present but authorization checks are missing, producing a prioritized finding with severity and remediation guidance.
Basic Auth-Specific Remediation in Fiber — concrete code fixes
To fix IDOR in Fiber while using Basic Auth, you must ensure that every data access call includes the authenticated subject as a mandatory filter. This means deriving the authenticated identity from the credentials and using it in all database or storage queries, rather than relying solely on route parameters. Below are concrete, idiomatic examples that demonstrate secure handling in Fiber.
First, a secure pattern for decoding Basic Auth and scoping queries by username:
// secure_handler.go
package main
import (
"database/sql"
"encoding/base64"
"fmt"
"net/http"
"strings"
"github.com/gofiber/fiber/v2"
)
type UserRecord struct {
ID int `json:"id"`
Name string `json:"name"`
// other fields
}
func getProfile(db *sql.DB) fiber.Handler {
return func(c *fiber.Ctx) error {
// Extract and parse Basic Auth header
auth := c.Get("Authorization")
if auth == "" {
return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{"error": "authorization header required"})
}
parts := strings.SplitN(auth, " ", 2)
if len(parts) != 2 || parts[0] != "Basic" {
return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{"error": "invalid authorization header format"})
}
decoded, err := base64.StdEncoding.DecodeString(parts[1])
if err != nil {
return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{"error": "invalid credentials"})
}
creds := strings.SplitN(string(decoded), ":", 2)
if len(creds) != 2 {
return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{"error": "invalid credentials"})
}
username := creds[0]
// Use the authenticated username to scope the lookup instead of trusting URL ID
var record UserRecord
row := db.QueryRow("SELECT id, name FROM users WHERE username = $1 AND deleted_at IS NULL", username)
if err := row.Scan(&record.ID, &record.Name); err != nil {
if err == sql.ErrNoRows {
return c.Status(fiber.StatusNotFound).JSON(fiber.Map{"error": "profile not found"})
}
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "database error"})
}
return c.JSON(record)
}
}
In this example, the route might be defined as app.Get("/profile", getProfile(db)). The key remediation is that the query filters by username derived from Basic Auth rather than accepting an ID from the URL. This prevents attackers from changing the URL to reference another user’s record. If your data model uses numeric IDs internally, you should still look up the record by username and then optionally include the ID in the response, but never use the user-supplied ID for authorization decisions.
For cases where a numeric resource ID must be part of the URL (for example, /projects/456), you must verify that the authenticated user owns or has permission for that specific ID. This requires a permission or membership check before returning data:
// idor_protected_handler.go
package main
import (
"database/sql"
"fmt"
"net/http"
"strconv"
"github.com/gofiber/fiber/v2"
)
func getProject(db *sql.DB) fiber.Handler {
return func(c *fiber.Ctx) error {
auth := c.Get("Authorization")
if auth == "" {
return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{"error": "authorization header required"})
}
parts := strings.SplitN(auth, " ", 2)
if len(parts) != 2 || parts[0] != "Basic" {
return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{"error": "invalid authorization header format"})
}
decoded, err := base64.StdEncoding.DecodeString(parts[1])
if err != nil {
return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{"error": "invalid credentials"})
}
creds := strings.SplitN(string(decoded), ":", 2)
username := creds[0]
projectID, err := strconv.Atoi(c.Params("id"))
if err != nil {
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "invalid project ID"})
}
// Verify ownership or permission before returning data
var projectName string
row := db.QueryRow(`
SELECT p.name FROM projects p
JOIN memberships m ON p.id = m.project_id
WHERE p.id = $1 AND m.username = $2 AND m.role IS NOT NULL`,
projectID, username)
if err := row.Scan(&projectName); err != nil {
if err == sql.ErrNoRows {
return c.Status(fiber.StatusForbidden).JSON(fiber.Map{"error": "access denied"})
}
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "database error"})
}
return c.JSON(fiber.Map{"project_id": projectID, "name": projectName})
}
}
Here, the numeric ID from the URL is used only after confirming that the authenticated user has a valid membership row. This approach aligns with remediation guidance from scans performed by tools like middleBrick, which highlight the need to correlate authentication context with object-level permissions. By always filtering on the authenticated identity, you eliminate the direct object reference that enables IDOR.
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 |