Echo Security Checklist
Introduction
Security is a critical aspect of web application development. When building applications with the Echo framework, following proper security practices helps protect your application, its data, and your users from various threats and vulnerabilities.
This guide provides a comprehensive security checklist for Echo applications, covering essential security measures and best practices. By implementing these recommendations, you can significantly enhance the security posture of your Echo applications.
Why Security Matters
Before diving into specific security measures, it's important to understand why security should be a priority:
- Protects sensitive user data
- Prevents unauthorized access to your application
- Maintains user trust and confidence
- Avoids potential legal and financial repercussions
- Ensures business continuity
Essential Security Checklist
1. Input Validation
Always validate and sanitize user input to prevent injection attacks like SQL injection, cross-site scripting (XSS), and command injection.
// Bad example - no validation
func createUser(c echo.Context) error {
username := c.FormValue("username")
// Using username directly without validation
return c.String(http.StatusOK, "User created: "+username)
}
// Good example - with validation
func createUser(c echo.Context) error {
username := c.FormValue("username")
// Validate username
if len(username) < 3 || len(username) > 50 {
return c.JSON(http.StatusBadRequest, map[string]string{
"error": "Username must be between 3 and 50 characters",
})
}
// Additional validation to prevent injection
if strings.ContainsAny(username, "<>&'\"\\/") {
return c.JSON(http.StatusBadRequest, map[string]string{
"error": "Username contains invalid characters",
})
}
return c.String(http.StatusOK, "User created: "+username)
}
2. Authentication and Authorization
Implement proper authentication and authorization mechanisms to control access to your application.
Authentication Example
import (
"github.com/labstack/echo/v4"
"github.com/labstack/echo/v4/middleware"
)
func main() {
e := echo.New()
// JWT middleware
e.Use(middleware.JWTWithConfig(middleware.JWTConfig{
SigningKey: []byte("your-secret-key"),
TokenLookup: "header:Authorization",
AuthScheme: "Bearer",
}))
// Protected route
e.GET("/protected", protectedHandler)
e.Logger.Fatal(e.Start(":8080"))
}
func protectedHandler(c echo.Context) error {
user := c.Get("user").(*jwt.Token)
claims := user.Claims.(jwt.MapClaims)
name := claims["name"].(string)
return c.String(http.StatusOK, "Welcome "+name+"!")
}
Authorization Example
// Role-based middleware
func RoleMiddleware(roles ...string) echo.MiddlewareFunc {
return func(next echo.HandlerFunc) echo.HandlerFunc {
return func(c echo.Context) error {
user := c.Get("user").(*jwt.Token)
claims := user.Claims.(jwt.MapClaims)
// Check if the user has any of the required roles
userRoles := claims["roles"].([]interface{})
for _, userRole := range userRoles {
for _, role := range roles {
if userRole.(string) == role {
return next(c)
}
}
}
return c.JSON(http.StatusForbidden, map[string]string{
"error": "Insufficient permissions",
})
}
}
}
// Usage
adminGroup := e.Group("/admin")
adminGroup.Use(RoleMiddleware("admin"))
adminGroup.GET("/dashboard", adminDashboard)
3. HTTPS Implementation
Always use HTTPS in production to encrypt data in transit.
func main() {
e := echo.New()
// Routes setup
// ...
// Start server with TLS (HTTPS)
e.Logger.Fatal(e.StartTLS(":8443", "cert.pem", "key.pem"))
}
4. CORS Configuration
Configure Cross-Origin Resource Sharing (CORS) properly to control which domains can access your API.
func main() {
e := echo.New()
// Configure CORS
e.Use(middleware.CORSWithConfig(middleware.CORSConfig{
AllowOrigins: []string{"https://yourfrontenddomain.com"},
AllowMethods: []string{http.MethodGet, http.MethodPut, http.MethodPost, http.MethodDelete},
AllowHeaders: []string{echo.HeaderOrigin, echo.HeaderContentType, echo.HeaderAccept, echo.HeaderAuthorization},
AllowCredentials: true,
}))
// Routes setup
// ...
e.Logger.Fatal(e.Start(":8080"))
}
5. Rate Limiting
Implement rate limiting to prevent abuse and DoS attacks.
func main() {
e := echo.New()
// Rate limiter - 10 requests per second
e.Use(middleware.RateLimiter(middleware.NewRateLimiterMemoryStore(10)))
// Routes setup
// ...
e.Logger.Fatal(e.Start(":8080"))
}
6. Secure Headers
Add security headers to protect against common attacks.
func main() {
e := echo.New()
// Secure middleware
e.Use(middleware.SecureWithConfig(middleware.SecureConfig{
XSSProtection: "1; mode=block",
ContentTypeNosniff: "nosniff",
XFrameOptions: "SAMEORIGIN",
HSTSMaxAge: 31536000, // 1 year
HSTSExcludeSubdomains: false,
ContentSecurityPolicy: "default-src 'self'",
}))
// Routes setup
// ...
e.Logger.Fatal(e.Start(":8080"))
}
7. CSRF Protection
Implement CSRF protection for forms and state-changing operations.
func main() {
e := echo.New()
// CSRF middleware
e.Use(middleware.CSRFWithConfig(middleware.CSRFConfig{
TokenLookup: "form:_csrf",
CookieName: "csrf_token",
CookiePath: "/",
CookieSecure: true, // For HTTPS
CookieHTTPOnly: true,
}))
// Routes setup
e.GET("/form", showForm)
e.POST("/form", processForm)
e.Logger.Fatal(e.Start(":8080"))
}
func showForm(c echo.Context) error {
token := c.Get(middleware.DefaultCSRFConfig.ContextKey).(string)
return c.HTML(http.StatusOK, `
<form method="POST">
<input type="hidden" name="_csrf" value="`+token+`">
<input type="text" name="name">
<button type="submit">Submit</button>
</form>
`)
}
func processForm(c echo.Context) error {
return c.String(http.StatusOK, "Form processed successfully")
}
8. Password Storage
Never store passwords in plain text. Use secure hashing algorithms like bcrypt.
import "golang.org/x/crypto/bcrypt"
// Hash a password for storage
func hashPassword(password string) (string, error) {
bytes, err := bcrypt.GenerateFromPassword([]byte(password), 14)
return string(bytes), err
}
// Check if the provided password matches the stored hash
func checkPasswordHash(password, hash string) bool {
err := bcrypt.CompareHashAndPassword([]byte(hash), []byte(password))
return err == nil
}
// Example usage in a registration handler
func registerUser(c echo.Context) error {
username := c.FormValue("username")
password := c.FormValue("password")
// Validate input
// ...
// Hash password
hashedPassword, err := hashPassword(password)
if err != nil {
return c.JSON(http.StatusInternalServerError, map[string]string{
"error": "Failed to process password",
})
}
// Store user in database with hashedPassword
// ...
return c.JSON(http.StatusCreated, map[string]string{
"message": "User registered successfully",
})
}
9. Error Handling
Implement proper error handling to avoid exposing sensitive information.
func main() {
e := echo.New()
// Custom error handler
e.HTTPErrorHandler = customErrorHandler
// Routes setup
// ...
e.Logger.Fatal(e.Start(":8080"))
}
func customErrorHandler(err error, c echo.Context) {
code := http.StatusInternalServerError
if he, ok := err.(*echo.HTTPError); ok {
code = he.Code
}
// Log the detailed error for debugging
c.Logger().Error(err)
// Return a generic error message to the client
if code == http.StatusInternalServerError {
c.JSON(code, map[string]string{
"error": "Internal server error",
})
return
}
c.JSON(code, map[string]string{
"error": err.Error(),
})
}
10. Database Security
Use parameterized queries to prevent SQL injection.
import (
"database/sql"
_ "github.com/go-sql-driver/mysql"
)
// Bad example - vulnerable to SQL injection
func getUser(username string, db *sql.DB) (*User, error) {
// DANGER: Direct string concatenation
query := "SELECT id, name, email FROM users WHERE username = '" + username + "'"
// Execute query...
}
// Good example - using parameterized queries
func getUser(username string, db *sql.DB) (*User, error) {
// Safe: Using parameters
query := "SELECT id, name, email FROM users WHERE username = ?"
var user User
err := db.QueryRow(query, username).Scan(&user.ID, &user.Name, &user.Email)
return &user, err
}
11. Logging and Monitoring
Implement proper logging and monitoring for security events.
import (
"github.com/labstack/echo/v4"
"github.com/labstack/echo/v4/middleware"
"github.com/labstack/gommon/log"
)
func main() {
e := echo.New()
// Set log level
e.Logger.SetLevel(log.INFO)
// Add logger middleware
e.Use(middleware.LoggerWithConfig(middleware.LoggerConfig{
Format: "${time_rfc3339} ${remote_ip} ${method} ${uri} ${status} ${latency_human}\n",
}))
// Add a custom logger for authentication events
e.Use(func(next echo.HandlerFunc) echo.HandlerFunc {
return func(c echo.Context) error {
// Check if this is an authentication endpoint
if c.Path() == "/login" || c.Path() == "/logout" {
e.Logger.Infof("Authentication event: %s %s from IP %s",
c.Request().Method, c.Path(), c.RealIP())
}
return next(c)
}
})
// Routes setup
// ...
e.Logger.Fatal(e.Start(":8080"))
}
12. Dependency Management
Regularly update dependencies to address security vulnerabilities.
# Update dependencies
go get -u ./...
# Use tools like Nancy to check for vulnerabilities
go list -m -json all | nancy sleuth
Real-World Application: Secure API Implementation
Let's put together several security practices into a comprehensive example of a secure API implementation with Echo:
package main
import (
"github.com/labstack/echo/v4"
"github.com/labstack/echo/v4/middleware"
"github.com/golang-jwt/jwt/v4"
"golang.org/x/crypto/bcrypt"
"net/http"
"time"
)
type User struct {
ID int `json:"id"`
Username string `json:"username"`
Password string `json:"-"` // Never return password in responses
Roles []string `json:"roles"`
}
var users = map[string]*User{
"admin": {
ID: 1,
Username: "admin",
Password: "$2a$14$ajq8Q7fbtFRQvXpdCq7Jcuy.Rx1h/L4J60Otx.gyNLbAYctGMJ9tK", // "secret"
Roles: []string{"admin"},
},
"user": {
ID: 2,
Username: "user",
Password: "$2a$14$ajq8Q7fbtFRQvXpdCq7Jcuy.Rx1h/L4J60Otx.gyNLbAYctGMJ9tK", // "secret"
Roles: []string{"user"},
},
}
func main() {
e := echo.New()
// Middleware
e.Use(middleware.Logger())
e.Use(middleware.Recover())
e.Use(middleware.SecureWithConfig(middleware.SecureConfig{
XSSProtection: "1; mode=block",
ContentTypeNosniff: "nosniff",
XFrameOptions: "SAMEORIGIN",
HSTSMaxAge: 31536000,
ContentSecurityPolicy: "default-src 'self'",
}))
e.Use(middleware.RateLimiter(middleware.NewRateLimiterMemoryStore(20)))
e.Use(middleware.CORSWithConfig(middleware.CORSConfig{
AllowOrigins: []string{"https://yourapplication.com"},
AllowMethods: []string{http.MethodGet, http.MethodPost, http.MethodPut, http.MethodDelete},
AllowHeaders: []string{echo.HeaderOrigin, echo.HeaderContentType, echo.HeaderAccept, echo.HeaderAuthorization},
}))
// Routes
e.POST("/login", login)
// Authenticated routes
r := e.Group("/api")
r.Use(middleware.JWTWithConfig(middleware.JWTConfig{
SigningKey: []byte("your-secret-key"),
TokenLookup: "header:Authorization",
AuthScheme: "Bearer",
}))
r.GET("/user", getUser)
// Admin-only routes
admin := r.Group("/admin")
admin.Use(requireAdmin)
admin.GET("/stats", getAdminStats)
// Start server
e.Logger.Fatal(e.Start(":8080"))
}
func login(c echo.Context) error {
username := c.FormValue("username")
password := c.FormValue("password")
// Input validation
if username == "" || password == "" {
return c.JSON(http.StatusBadRequest, map[string]string{
"error": "Username and password are required",
})
}
// Find user
user, exists := users[username]
if !exists {
return c.JSON(http.StatusUnauthorized, map[string]string{
"error": "Invalid credentials",
})
}
// Compare password
if !checkPasswordHash(password, user.Password) {
return c.JSON(http.StatusUnauthorized, map[string]string{
"error": "Invalid credentials",
})
}
// Generate JWT token
claims := jwt.MapClaims{
"id": user.ID,
"username": user.Username,
"roles": user.Roles,
"exp": time.Now().Add(time.Hour * 24).Unix(),
}
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
t, err := token.SignedString([]byte("your-secret-key"))
if err != nil {
return c.JSON(http.StatusInternalServerError, map[string]string{
"error": "Could not generate token",
})
}
return c.JSON(http.StatusOK, map[string]string{
"token": t,
})
}
func getUser(c echo.Context) error {
user := c.Get("user").(*jwt.Token)
claims := user.Claims.(jwt.MapClaims)
username := claims["username"].(string)
return c.JSON(http.StatusOK, map[string]interface{}{
"user": users[username],
})
}
func requireAdmin(next echo.HandlerFunc) echo.HandlerFunc {
return func(c echo.Context) error {
user := c.Get("user").(*jwt.Token)
claims := user.Claims.(jwt.MapClaims)
roles := claims["roles"].([]interface{})
isAdmin := false
for _, role := range roles {
if role.(string) == "admin" {
isAdmin = true
break
}
}
if !isAdmin {
return c.JSON(http.StatusForbidden, map[string]string{
"error": "Admin access required",
})
}
return next(c)
}
}
func getAdminStats(c echo.Context) error {
return c.JSON(http.StatusOK, map[string]interface{}{
"total_users": len(users),
"active": true,
"server_uptime": "3d 12h 4m",
})
}
func checkPasswordHash(password, hash string) bool {
err := bcrypt.CompareHashAndPassword([]byte(hash), []byte(password))
return err == nil
}
Summary
Security is a critical aspect of web application development. This checklist provides a foundation for building secure Echo applications, covering essential security measures like:
- Input validation and sanitization
- Authentication and authorization
- HTTPS implementation
- CORS configuration
- Rate limiting
- Secure headers
- CSRF protection
- Secure password storage
- Proper error handling
- Database security
- Logging and monitoring
- Regular dependency updates
By implementing these security measures, you can significantly enhance the security of your Echo applications and protect them against common web vulnerabilities and attacks.
Additional Resources
- Echo Framework Official Security Guide
- OWASP Top 10 Web Application Security Risks
- Go Security Best Practices
- JWT Authentication in Go
- NIST Password Guidelines
Exercises
- Security Audit: Perform a security audit on an existing Echo application using this checklist.
- Implement Authentication: Create a simple Echo application with JWT authentication.
- Add Input Validation: Practice adding input validation to all user input in an Echo application.
- Configure HTTPS: Set up an Echo application to use HTTPS with a self-signed certificate for development.
- Implement Role-Based Authorization: Extend the authentication system to include role-based authorization for different routes.
Remember, security is not a one-time task but an ongoing process. Regularly review and update your security practices to address new threats and vulnerabilities as they emerge.
If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)