Unicode Normalization in Echo Go
How Unicode Normalization Manifests in Echo Go
Unicode normalization attacks in Echo Go typically exploit the framework's handling of string inputs before they reach business logic. Echo Go's default behavior accepts raw request data without pre-processing, making it vulnerable to homograph attacks and duplicate character representations.
The most common manifestation occurs in path parameters. Consider this Echo Go route:
e.GET("/user/:id", func(c echo.Context) error {
userID := c.Param("id")
// Direct use of userID without normalization
return c.JSON(http.StatusOK, getUser(userID))
})An attacker can craft a request like /user/ⁱᴅ where the 'i' and 'd' characters use Unicode superscript forms (U+2071, U+1D31). These visually identical characters have different byte representations but may bypass string comparisons in your database layer.
Echo Go's context binding also introduces normalization vulnerabilities. When binding JSON or form data:
type User struct {
Email string `json:"email"`
}
e.POST("/register", func(c echo.Context) error {
var user User
if err := c.Bind(&user); err != nil {
return err
}
// Email used directly without normalization
return c.JSON(http.StatusOK, registerUser(user.Email))
})Attackers can register accounts with emails like test@ex́ample.com (where ́ is a combining acute accent) that appear identical to legitimate users but create separate database entries.
Header-based attacks are particularly effective in Echo Go. The framework's header parsing preserves Unicode characters:
e.Use(func(next echo.HandlerFunc) echo.HandlerFunc {
return func(c echo.Context) error {
authHeader := c.Request().Header.Get("Authorization")
// No normalization before validation
if authHeader != "Bearer valid-token" {
return echo.ErrUnauthorized
}
return next(c)
}
})Using Unicode variations like B̲e̲a̲r̲ (with combining underlines) can bypass simple string comparisons while appearing identical to administrators reviewing logs.
Query parameter handling in Echo Go similarly lacks built-in normalization. An endpoint like:
e.GET("/search", func(c echo.Context) error {
query := c.QueryParam("q")
// Direct use in database queries
return c.JSON(http.StatusOK, searchDatabase(query))
})Allows attackers to perform duplicate content attacks by submitting queries with different Unicode normal forms, potentially triggering multiple alerts or creating redundant data entries.
Echo Go-Specific Detection
Detecting Unicode normalization issues in Echo Go requires both manual code review and automated scanning. Start by examining all string inputs that undergo equality comparisons or database lookups.
Manual detection checklist for Echo Go applications:
- Search for all
c.Param(),c.QueryParam(),c.FormValue()calls - Identify string comparisons without normalization (
==,!=,strings.EqualFold) - Locate database queries using raw string parameters
- Find authentication logic comparing headers or tokens
- Check JSON/XML binding structures
Automated detection with middleBrick specifically identifies Echo Go vulnerabilities by:
middlebrick scan https://yourapi.com/api/v1/user/1
# Returns security score with Unicode normalization findings
# middleBrick tests multiple Unicode representations:
# - NFC (Canonical Decomposition, then Composition)
# - NFD (Canonical Decomposition)
# - NFKC (Compatibility Decomposition, then Composition)
# - NFKD (Compatibility Decomposition)middleBrick's Echo Go-specific scanner sends requests with Unicode variations and checks for:
- Authentication bypass with visually identical characters
- Duplicate account creation with different normal forms
- Path traversal using Unicode slashes
- Header injection with combining characters
The scanner reports findings with severity levels and provides exact request examples that trigger the vulnerability, making remediation straightforward.
For local testing, Echo Go developers can use curl to verify normalization issues:
# Test with NFC vs NFD forms
curl -v "https://yourapi.com/api/v1/user/á" # NFD form
curl -v "https://yourapi.com/api/v1/user/á" # NFC formIf both requests return different results or both succeed when only one should, you have a normalization vulnerability.
Echo Go-Specific Remediation
Echo Go provides several approaches to fix Unicode normalization vulnerabilities. The most effective solution is implementing consistent normalization across all string inputs.
Using Go's standard library for normalization:
import (
"golang.org/x/text/unicode/norm"
"github.com/labstack/echo/v4"
)
// Middleware for automatic normalization
func normalizeMiddleware(next echo.HandlerFunc) echo.HandlerFunc {
return func(c echo.Context) error {
// Normalize path parameters
params := c.ParamNames()
for _, name := range params {
value := c.Param(name)
normalized := norm.NFC.String(value)
c.SetParam(name, normalized)
}
// Normalize query parameters
query := c.QueryParams()
normalizedQuery := make(url.Values)
for key, values := range query {
normKey := norm.NFC.String(key)
var normValues []string
for _, val := range values {
normValues = append(normValues, norm.NFC.String(val))
}
normalizedQuery[normKey] = normValues
}
c.Set("normalizedQuery", normalizedQuery)
// Normalize headers
headers := c.Request().Header
for key, values := range headers {
normKey := norm.NFC.String(key)
var normValues []string
for _, val := range values {
normValues = append(normValues, norm.NFC.String(val))
}
c.Request().Header[normKey] = normValues
delete(c.Request().Header, key) // Remove original
}
return next(c)
}
}
e := echo.New()
e.Use(normalizeMiddleware)For database operations, normalize before queries:
func getUserID(c echo.Context) error {
userID := c.Param("id")
normalizedID := norm.NFC.String(userID)
var user User
err := db.QueryRow("SELECT * FROM users WHERE id = ?", normalizedID).Scan(&user)
if err != nil {
return echo.ErrNotFound
}
return c.JSON(http.StatusOK, user)
}Authentication middleware with normalization:
e.Use(func(next echo.HandlerFunc) echo.HandlerFunc {
return func(c echo.Context) error {
authHeader := c.Request().Header.Get("Authorization")
if authHeader == "" {
return echo.ErrUnauthorized
}
// Normalize before comparison
normalizedAuth := norm.NFC.String(authHeader)
if normalizedAuth != "Bearer valid-token" {
return echo.ErrUnauthorized
}
return next(c)
}
})For JSON binding, use custom unmarshalers:
type NormalizedUser struct {
Email string `json:"email"`
}
func (u *NormalizedUser) UnmarshalJSON(data []byte) error {
var temp struct {
Email string `json:"email"`
}
if err := json.Unmarshal(data, &temp); err != nil {
return err
}
u.Email = norm.NFC.String(temp.Email)
return nil
}Echo Go's middleware architecture makes it ideal for implementing a global normalization layer that processes all requests before they reach your business logic, ensuring consistent handling of Unicode characters throughout your application.