Privilege Escalation in Echo Go
How Privilege Escalation Manifests in Echo Go
Privilege escalation in Echo Go typically occurs through improper authorization checks in middleware and route handlers. A common pattern involves Echo's JWT middleware validating tokens but failing to verify user roles against the requested resource's ownership.
For example, consider an API endpoint that retrieves user data:
func getUser(c echo.Context) error {
id := c.Param("id")
user, err := models.GetUserByID(id)
if err != nil {
return echo.NewHTTPError(http.StatusNotFound, "User not found")
}
return c.JSON(http.StatusOK, user)
}This handler retrieves any user by ID without checking if the requester owns that data. An attacker could simply increment the ID parameter to access other users' information.
Echo's context binding makes this particularly dangerous. Consider a POST endpoint for updating user profiles:
type UpdateProfile struct {
Email string `json:"email" validate:"email"`
Bio string `json:"bio"`
}
func updateProfile(c echo.Context) error {
userID := c.Get("user_id").(int) // from JWT middleware
var input UpdateProfile
if err := c.Bind(&input); err != nil {
return echo.NewHTTPError(http.StatusBadRequest, err.Error())
}
user, err := models.GetUserByID(userID)
if err != nil {
return echo.NewHTTPError(http.StatusNotFound, "User not found")
}
user.Email = input.Email
user.Bio = input.Bio
if err := models.UpdateUser(user); err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Update failed")
}
return c.JSON(http.StatusOK, user)
}The vulnerability here is that userID comes from the JWT token, but if an attacker can craft a token with any user ID, they can update any profile. Echo's JWT middleware doesn't automatically validate token claims against resource ownership.
Another Echo-specific escalation vector involves Echo's group-based middleware. Developers often protect entire route groups:
adminGroup := v1.Group("/admin")
adminGroup.Use(middleware.JWTWithConfig(middleware.JWTConfig{
SigningKey: []byte("secret"),
}))
adminGroup.GET("/users", adminListUsers)
adminGroup.POST("/users", adminCreateUser)But if the JWT middleware only validates token structure and not specific admin claims, any authenticated user can access admin endpoints. Echo doesn't enforce role-based access control by default—developers must implement this manually.
Echo's parameter binding also creates escalation opportunities. Consider this endpoint:
func deleteUser(c echo.Context) error {
id := c.Param("id")
if err := models.DeleteUserByID(id); err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Delete failed")
}
return c.NoContent(http.StatusNoContent)
}An authenticated user could delete any account by changing the ID parameter. Echo's parameter extraction is straightforward but provides no built-in authorization checks.
Echo's middleware chain execution order matters for escalation prevention. If authentication middleware runs after authorization checks, the entire security model breaks:
// INSECURE - wrong order
adminGroup.GET("/users", adminListUsers)
adminGroup.Use(middleware.JWTWithConfig(config))The fix requires authentication middleware to run first, which Echo allows but doesn't enforce automatically.
Echo Go-Specific Detection
Detecting privilege escalation in Echo applications requires both static analysis and runtime scanning. Static analysis tools can identify patterns where Echo's context binding is used without proper authorization checks.
Using middleBrick's CLI for Echo-specific scanning:
npx middlebrick scan https://api.example.com --api-type echo --auth-type jwtmiddleBrick's Echo detection module specifically looks for:
- Echo context binding without ownership verification
- Echo group middleware that lacks role validation
- Echo parameter extraction used for resource identification without authorization
- Echo JWT middleware usage without claim validation
The scanner tests for BOLA (Broken Object Level Authorization) by attempting authenticated requests with modified resource IDs. For an endpoint like /users/:id, middleBrick will:
- Authenticate with a valid JWT token
- Request
/users/1(valid user) - Request
/users/999(likely invalid/another user) - Compare responses to detect information leakage
For Echo applications, middleBrick also analyzes OpenAPI specs if provided:
npx middlebrick scan https://api.example.com --spec openapi.jsonThis cross-references endpoint definitions with actual runtime behavior, identifying mismatches between documented security requirements and implementation.
Manual detection techniques for Echo apps include:
// Test for privilege escalation
func testEscalation(c echo.Context) error {
userID := c.Get("user_id").(int)
// Test if user can access others' data
otherUser, err := models.GetUserByID(userID + 1)
if err == nil {
return echo.NewHTTPError(http.StatusForbidden, "Privilege escalation possible")
}
return c.JSON(http.StatusOK, "Safe")
}Echo's middleware chain can be instrumented to log authorization failures:
func auditMiddleware(next echo.HandlerFunc) echo.HandlerFunc {
return func(c echo.Context) error {
start := time.Now()
err := next(c)
// Log authorization context
userID := c.Get("user_id")
path := c.Path()
method := c.Request().Method
log.Printf("Auth audit: user=%v path=%s method=%s duration=%v",
userID, path, method, time.Since(start))
return err
}
}Echo's built-in validator can be extended to check authorization:
validator := &CustomValidator{validator: validate}
echo.Validator = validatorThis allows validation to include authorization checks before data access occurs.
Echo Go-Specific Remediation
Remediating privilege escalation in Echo requires implementing proper authorization checks at the handler level. The most effective approach uses Echo's context to store user claims and verify resource ownership before data access.
Secure Echo handler pattern:
func getUser(c echo.Context) error {
id := c.Param("id")
// Get authenticated user ID from JWT claims
authUserID := c.Get("user_id").(int)
// Verify ownership or admin role
if id != strconv.Itoa(authUserID) {
// Check if user has admin role
roles := c.Get("roles").([]string)
if !contains(roles, "admin") {
return echo.NewHTTPError(http.StatusForbidden, "Access denied")
}
}
user, err := models.GetUserByID(id)
if err != nil {
return echo.NewHTTPError(http.StatusNotFound, "User not found")
}
return c.JSON(http.StatusOK, user)
}
func contains(arr []string, str string) bool {
for _, a := range arr {
if a == str {
return true
}
}
return false
}Echo's middleware can extract and validate JWT claims centrally:
func authMiddleware(next echo.HandlerFunc) echo.HandlerFunc {
return func(c echo.Context) error {
token := c.Request().Header.Get("Authorization")
if token == "" {
return echo.NewHTTPError(http.StatusUnauthorized, "Missing token")
}
claims, err := parseJWT(token)
if err != nil {
return echo.NewHTTPError(http.StatusUnauthorized, "Invalid token")
}
// Store claims in context for downstream handlers
c.Set("user_id", claims.UserID)
c.Set("roles", claims.Roles)
c.Set("email", claims.Email)
return next(c)
}
}For Echo group protection with role-based access:
func roleMiddleware(allowedRoles ...string) echo.MiddlewareFunc {
return func(next echo.HandlerFunc) echo.HandlerFunc {
return func(c echo.Context) error {
userRoles := c.Get("roles").([]string)
for _, role := range allowedRoles {
if contains(userRoles, role) {
return next(c)
}
}
return echo.NewHTTPError(http.StatusForbidden, "Insufficient privileges")
}
}
}
// Usage
adminGroup := v1.Group("/admin")
adminGroup.Use(authMiddleware)
adminGroup.Use(roleMiddleware("admin"))
adminGroup.GET("/users", adminListUsers)
adminGroup.POST("/users", adminCreateUser)Echo's parameter binding can be made secure with ownership verification:
func updateUserProfile(c echo.Context) error {
userID := c.Get("user_id").(int)
var input UpdateProfile
if err := c.Bind(&input); err != nil {
return echo.NewHTTPError(http.StatusBadRequest, err.Error())
}
// Verify user owns this profile
if c.Param("id") != strconv.Itoa(userID) {
return echo.NewHTTPError(http.StatusForbidden, "Cannot update other users' profiles")
}
user, err := models.GetUserByID(strconv.Itoa(userID))
if err != nil {
return echo.NewHTTPError(http.StatusNotFound, "User not found")
}
user.Email = input.Email
user.Bio = input.Bio
if err := models.UpdateUser(user); err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Update failed")
}
return c.JSON(http.StatusOK, user)
}Echo's HTTP error handling can provide consistent authorization failure responses:
func forbidden(c echo.Context, message string) error {
return echo.NewHTTPError(http.StatusForbidden, message)
}
// Centralized authorization check
func authorizeResource(c echo.Context, resourceOwnerID int) bool {
authUserID := c.Get("user_id").(int)
if authUserID == resourceOwnerID {
return true
}
roles := c.Get("roles").([]string)
return contains(roles, "admin")
}Echo's middleware chain ordering is critical—authentication must always precede authorization:
e := echo.New()
// Correct order: auth first, then authorization
v1 := e.Group("/api/v1")
v1.Use(authMiddleware)
v1.Use(roleMiddleware("user", "admin"))
v1.GET("/users/:id", getUser)
v1.POST("/users/:id", updateUserProfile)