Security
Overview
Security is a critical aspect of web application development. The framework provides built-in security features and patterns to protect against common vulnerabilities.
Authentication & Authorization
JWT Authentication
type AuthService struct {
secretKey []byte
userFetcher *srv.UserFetcher
}
func NewAuthService(secretKey string, userFetcher *srv.UserFetcher) *AuthService {
return &AuthService{
secretKey: []byte(secretKey),
userFetcher: userFetcher,
}
}
func (as *AuthService) GenerateToken(user *mdl.User) (string, error) {
claims := jwt.MapClaims{
"user_id": user.Id,
"email": user.Email,
"exp": time.Now().Add(time.Hour * 24).Unix(),
"iat": time.Now().Unix(),
}
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
return token.SignedString(as.secretKey)
}
func (as *AuthService) ValidateToken(tokenString string) (*mdl.User, error) {
token, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) {
if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"])
}
return as.secretKey, nil
})
if err != nil || !token.Valid {
return nil, errors.New("invalid token")
}
claims, ok := token.Claims.(jwt.MapClaims)
if !ok {
return nil, errors.New("invalid token claims")
}
userID := claims["user_id"].(string)
user, exists, err := as.userFetcher.FindOneById(context.Background(), userID, nil)
if err != nil || !exists {
return nil, errors.New("user not found")
}
return user, nil
}
Password Hashing
type PasswordService struct {
cost int
}
func NewPasswordService() *PasswordService {
return &PasswordService{cost: bcrypt.DefaultCost}
}
func (ps *PasswordService) HashPassword(password string) (string, error) {
bytes, err := bcrypt.GenerateFromPassword([]byte(password), ps.cost)
return string(bytes), err
}
func (ps *PasswordService) CheckPassword(password, hash string) bool {
err := bcrypt.CompareHashAndPassword([]byte(hash), []byte(password))
return err == nil
}
func (ps *PasswordService) ValidatePasswordStrength(password string) error {
if len(password) < 8 {
return errors.New("password must be at least 8 characters long")
}
hasUpper := regexp.MustCompile(`[A-Z]`).MatchString(password)
hasLower := regexp.MustCompile(`[a-z]`).MatchString(password)
hasNumber := regexp.MustCompile(`[0-9]`).MatchString(password)
hasSpecial := regexp.MustCompile(`[!@#$%^&*()_+\-=\[\]{};':"\\|,.<>\/?]`).MatchString(password)
if !hasUpper || !hasLower || !hasNumber || !hasSpecial {
return errors.New("password must contain uppercase, lowercase, number, and special character")
}
return nil
}
Role-Based Access Control
type Permission string
const (
PermissionCreateGarden Permission = "garden:create"
PermissionReadGarden Permission = "garden:read"
PermissionUpdateGarden Permission = "garden:update"
PermissionDeleteGarden Permission = "garden:delete"
PermissionManageUsers Permission = "user:manage"
)
type Role struct {
Name string
Permissions []Permission
}
var Roles = map[string]Role{
"admin": {
Name: "admin",
Permissions: []Permission{
PermissionCreateGarden,
PermissionReadGarden,
PermissionUpdateGarden,
PermissionDeleteGarden,
PermissionManageUsers,
},
},
"user": {
Name: "user",
Permissions: []Permission{
PermissionCreateGarden,
PermissionReadGarden,
PermissionUpdateGarden,
},
},
"viewer": {
Name: "viewer",
Permissions: []Permission{
PermissionReadGarden,
},
},
}
func (u *mdl.User) HasPermission(permission Permission) bool {
role, exists := Roles[u.Role]
if !exists {
return false
}
for _, p := range role.Permissions {
if p == permission {
return true
}
}
return false
}
// Middleware for permission checking
func RequirePermission(permission Permission) gin.HandlerFunc {
return func(c *gin.Context) {
user, exists := c.Get("current_user")
if !exists {
c.JSON(401, gin.H{"error": "Authentication required"})
c.Abort()
return
}
if !user.(*mdl.User).HasPermission(permission) {
c.JSON(403, gin.H{"error": "Insufficient permissions"})
c.Abort()
return
}
c.Next()
}
}
Input Validation & Sanitization
SQL Injection Prevention
// Always use parameterized queries
func (f *GardenFetcher) FindByName(ctx context.Context, name string) ([]*mdl.Garden, error) {
// ✅ Correct - parameterized query
query := "SELECT * FROM gardens WHERE name = ?"
rows, err := f.db.QueryContext(ctx, query, name)
// ❌ Never do this - SQL injection vulnerability
// query := fmt.Sprintf("SELECT * FROM gardens WHERE name = '%s'", name)
// rows, err := f.db.QueryContext(ctx, query)
if err != nil {
return nil, err
}
defer rows.Close()
return f.scanRows(rows)
}
// For dynamic WHERE clauses, use query builders
func (f *GardenFetcher) FindWithFilters(ctx context.Context, filters map[string]interface{}) ([]*mdl.Garden, error) {
query := "SELECT * FROM gardens WHERE 1=1"
args := make([]interface{}, 0)
for field, value := range filters {
switch field {
case "name":
query += " AND name = ?"
args = append(args, value)
case "active":
query += " AND active = ?"
args = append(args, value)
case "user_id":
query += " AND user_id = ?"
args = append(args, value)
}
}
rows, err := f.db.QueryContext(ctx, query, args...)
if err != nil {
return nil, err
}
defer rows.Close()
return f.scanRows(rows)
}
XSS Prevention
import "html"
// HTML escaping for templates
func EscapeHTML(input string) string {
return html.EscapeString(input)
}
// Custom template function for safe HTML
func SetupTemplates() *gin.Engine {
router := gin.Default()
router.SetFuncMap(template.FuncMap{
"escape": EscapeHTML,
"safeHTML": func(input string) template.HTML {
// Only use for trusted content
return template.HTML(input)
},
})
return router
}
// Content Security Policy middleware
func CSPMiddleware() gin.HandlerFunc {
return func(c *gin.Context) {
csp := "default-src 'self'; " +
"script-src 'self' 'unsafe-inline' https://cdn.tailwindcss.com; " +
"style-src 'self' 'unsafe-inline' https://fonts.googleapis.com; " +
"font-src 'self' https://fonts.gstatic.com; " +
"img-src 'self' data: https:; " +
"connect-src 'self'"
c.Header("Content-Security-Policy", csp)
c.Next()
}
}
Input Sanitization
import (
"regexp"
"strings"
"unicode"
)
type Sanitizer struct {
htmlTagRegex *regexp.Regexp
scriptRegex *regexp.Regexp
sqlKeywords []string
}
func NewSanitizer() *Sanitizer {
return &Sanitizer{
htmlTagRegex: regexp.MustCompile(`<[^>]*>`),
scriptRegex: regexp.MustCompile(`(?i)script`),
sqlKeywords: []string{"SELECT", "INSERT", "UPDATE", "DELETE", "DROP", "UNION"},
}
}
func (s *Sanitizer) SanitizeString(input string) string {
// Remove HTML tags
cleaned := s.htmlTagRegex.ReplaceAllString(input, "")
// Remove script content
cleaned = s.scriptRegex.ReplaceAllString(cleaned, "")
// Trim whitespace
cleaned = strings.TrimSpace(cleaned)
return cleaned
}
func (s *Sanitizer) ValidateNoSQLInjection(input string) error {
upperInput := strings.ToUpper(input)
for _, keyword := range s.sqlKeywords {
if strings.Contains(upperInput, keyword) {
return errors.New("potentially harmful SQL keyword detected")
}
}
return nil
}
func (s *Sanitizer) SanitizeFilename(filename string) string {
// Remove path separators and dangerous characters
cleaned := regexp.MustCompile(`[<>:"/\\|?*]`).ReplaceAllString(filename, "")
// Remove leading dots and spaces
cleaned = strings.TrimLeftFunc(cleaned, func(r rune) bool {
return r == '.' || unicode.IsSpace(r)
})
return cleaned
}
CSRF Protection
CSRF Token Implementation
type CSRFService struct {
secretKey []byte
}
func NewCSRFService(secretKey string) *CSRFService {
return &CSRFService{secretKey: []byte(secretKey)}
}
func (cs *CSRFService) GenerateToken(sessionID string) string {
h := hmac.New(sha256.New, cs.secretKey)
h.Write([]byte(sessionID))
h.Write([]byte(time.Now().Format("2006-01-02")))
return base64.URLEncoding.EncodeToString(h.Sum(nil))
}
func (cs *CSRFService) ValidateToken(token, sessionID string) bool {
expectedToken := cs.GenerateToken(sessionID)
return hmac.Equal([]byte(token), []byte(expectedToken))
}
// CSRF middleware
func CSRFMiddleware(csrfService *CSRFService) gin.HandlerFunc {
return func(c *gin.Context) {
// Skip GET, HEAD, OPTIONS requests
if c.Request.Method == "GET" || c.Request.Method == "HEAD" || c.Request.Method == "OPTIONS" {
c.Next()
return
}
sessionID := c.GetString("session_id")
if sessionID == "" {
c.JSON(403, gin.H{"error": "Session required"})
c.Abort()
return
}
token := c.GetHeader("X-CSRF-Token")
if token == "" {
token = c.PostForm("_token")
}
if !csrfService.ValidateToken(token, sessionID) {
c.JSON(403, gin.H{"error": "Invalid CSRF token"})
c.Abort()
return
}
c.Next()
}
}
Rate Limiting
Request Rate Limiting
import "golang.org/x/time/rate"
type RateLimiter struct {
limiters map[string]*rate.Limiter
mutex sync.RWMutex
rate rate.Limit
burst int
}
func NewRateLimiter(requests int, window time.Duration) *RateLimiter {
return &RateLimiter{
limiters: make(map[string]*rate.Limiter),
rate: rate.Every(window / time.Duration(requests)),
burst: requests,
}
}
func (rl *RateLimiter) GetLimiter(key string) *rate.Limiter {
rl.mutex.Lock()
defer rl.mutex.Unlock()
limiter, exists := rl.limiters[key]
if !exists {
limiter = rate.NewLimiter(rl.rate, rl.burst)
rl.limiters[key] = limiter
}
return limiter
}
func (rl *RateLimiter) Allow(key string) bool {
limiter := rl.GetLimiter(key)
return limiter.Allow()
}
// Rate limiting middleware
func RateLimitMiddleware(limiter *RateLimiter) gin.HandlerFunc {
return func(c *gin.Context) {
key := c.ClientIP()
if !limiter.Allow(key) {
c.JSON(429, gin.H{
"error": "Rate limit exceeded",
"retry_after": "60s",
})
c.Abort()
return
}
c.Next()
}
}
File Upload Security
Secure File Uploads
type FileUploadService struct {
allowedExtensions []string
allowedMimeTypes []string
maxFileSize int64
uploadDir string
sanitizer *Sanitizer
}
func NewFileUploadService(uploadDir string) *FileUploadService {
return &FileUploadService{
allowedExtensions: []string{".jpg", ".jpeg", ".png", ".gif", ".pdf", ".doc", ".docx"},
allowedMimeTypes: []string{"image/jpeg", "image/png", "image/gif", "application/pdf"},
maxFileSize: 10 << 20, // 10MB
uploadDir: uploadDir,
sanitizer: NewSanitizer(),
}
}
func (fus *FileUploadService) ValidateFile(fileHeader *multipart.FileHeader) error {
// Check file size
if fileHeader.Size > fus.maxFileSize {
return errors.New("file size exceeds limit")
}
// Check file extension
ext := strings.ToLower(filepath.Ext(fileHeader.Filename))
if !fus.isAllowedExtension(ext) {
return errors.New("file type not allowed")
}
// Open file to check MIME type
file, err := fileHeader.Open()
if err != nil {
return err
}
defer file.Close()
// Read first 512 bytes to detect MIME type
buffer := make([]byte, 512)
_, err = file.Read(buffer)
if err != nil {
return err
}
mimeType := http.DetectContentType(buffer)
if !fus.isAllowedMimeType(mimeType) {
return errors.New("file content type not allowed")
}
return nil
}
func (fus *FileUploadService) SaveFile(fileHeader *multipart.FileHeader, userID string) (string, error) {
if err := fus.ValidateFile(fileHeader); err != nil {
return "", err
}
// Generate secure filename
ext := filepath.Ext(fileHeader.Filename)
filename := fmt.Sprintf("%s_%d%s",
generateSecureID(),
time.Now().Unix(),
ext)
// Create user-specific directory
userDir := filepath.Join(fus.uploadDir, userID)
if err := os.MkdirAll(userDir, 0755); err != nil {
return "", err
}
filepath := filepath.Join(userDir, filename)
// Save file
src, err := fileHeader.Open()
if err != nil {
return "", err
}
defer src.Close()
dst, err := os.Create(filepath)
if err != nil {
return "", err
}
defer dst.Close()
_, err = io.Copy(dst, src)
return filename, err
}
func generateSecureID() string {
b := make([]byte, 16)
rand.Read(b)
return hex.EncodeToString(b)
}
Security Headers
Security Headers Middleware
func SecurityHeadersMiddleware() gin.HandlerFunc {
return func(c *gin.Context) {
// Prevent MIME sniffing
c.Header("X-Content-Type-Options", "nosniff")
// Prevent clickjacking
c.Header("X-Frame-Options", "DENY")
// Enable XSS protection
c.Header("X-XSS-Protection", "1; mode=block")
// Enforce HTTPS
c.Header("Strict-Transport-Security", "max-age=31536000; includeSubDomains")
// Control referrer information
c.Header("Referrer-Policy", "strict-origin-when-cross-origin")
// Permissions policy
c.Header("Permissions-Policy", "geolocation=(), microphone=(), camera=()")
c.Next()
}
}
Audit Logging
Security Event Logging
type SecurityLogger struct {
logger *log.Logger
}
func NewSecurityLogger(logger *log.Logger) *SecurityLogger {
return &SecurityLogger{logger: logger}
}
func (sl *SecurityLogger) LogSecurityEvent(event string, userID string, details map[string]interface{}) {
logEntry := map[string]interface{}{
"timestamp": time.Now().UTC(),
"event": event,
"user_id": userID,
"details": details,
}
jsonData, _ := json.Marshal(logEntry)
sl.logger.Printf("SECURITY: %s", string(jsonData))
}
// Usage in authentication
func (as *AuthService) Login(email, password string, clientIP string) (*mdl.User, string, error) {
user, exists, err := as.userFetcher.FindOneByEmail(context.Background(), email, nil)
if err != nil || !exists {
as.securityLogger.LogSecurityEvent("login_failed", "", map[string]interface{}{
"email": email,
"client_ip": clientIP,
"reason": "user_not_found",
})
return nil, "", errors.New("invalid credentials")
}
if !as.passwordService.CheckPassword(password, user.PasswordHash) {
as.securityLogger.LogSecurityEvent("login_failed", user.Id, map[string]interface{}{
"client_ip": clientIP,
"reason": "invalid_password",
})
return nil, "", errors.New("invalid credentials")
}
token, err := as.GenerateToken(user)
if err != nil {
return nil, "", err
}
as.securityLogger.LogSecurityEvent("login_success", user.Id, map[string]interface{}{
"client_ip": clientIP,
})
return user, token, nil
}
Security should be built into every layer of your application. Regular security audits and staying updated with the latest security practices are essential for maintaining a secure application.