| name | security-review |
| description | Use this skill when adding authentication, handling user input, working with secrets, creating API endpoints, or implementing payment/sensitive features. Provides comprehensive security checklist and patterns. |
Security Review Skill (Go Edition)
This skill ensures Go code follows security best practices and identifies potential vulnerabilities specific to Go applications.
When to Activate
- Implementing authentication or authorization in Go web applications
- Handling user input or file uploads in Gin/Go HTTP handlers
- Creating new API endpoints in Go
- Working with secrets or credentials in Go configuration
- Implementing payment or sensitive features in Go
- Storing or transmitting sensitive data
- Integrating third-party APIs with Go clients
Security Checklist
1. Secrets Management
❌ NEVER Do This
const apiKey = "sk-proj-xxxxx"
const dbPassword = "password123"
type Config struct {
JWTSecret string `json:"jwt_secret"`
}
✅ ALWAYS Do This
import (
"os"
"fmt"
)
jwtSecret := os.Getenv("JWT_SECRET")
if jwtSecret == "" {
return fmt.Errorf("JWT_SECRET environment variable not set")
}
type Config struct {
JWTSecret string `env:"JWT_SECRET,required"`
DatabaseURL string `env:"DATABASE_URL,required"`
APIKey string `env:"API_KEY"`
}
import "github.com/caarlos0/env/v6"
var cfg Config
if err := env.Parse(&cfg); err != nil {
log.Fatal("Failed to parse config:", err)
}
Verification Steps
2. Input Validation
Always Validate User Input
type CreateUserRequest struct {
Email string `json:"email" binding:"required,email"`
Name string `json:"name" binding:"required,min=1,max=100"`
Age int `json:"age" binding:"required,min=0,max=150"`
Password string `json:"password" binding:"required,min=8"`
}
func CreateUser(c *gin.Context) {
var req CreateUserRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(400, gin.H{"error": "Invalid input", "details": err.Error()})
return
}
user, err := service.CreateUser(req)
if err != nil {
c.JSON(500, gin.H{"error": "Internal server error"})
return
}
c.JSON(200, gin.H{"data": user})
}
import "github.com/go-playground/validator/v10"
var validate = validator.New()
func validateUser(req CreateUserRequest) error {
if err := validate.Struct(req); err != nil {
return fmt.Errorf("validation failed: %w", err)
}
if strings.Contains(strings.ToLower(req.Name), "admin") {
return fmt.Errorf("name cannot contain 'admin'")
}
return nil
}
File Upload Validation
import (
"mime/multipart"
"path/filepath"
"strings"
)
func validateFileUpload(fileHeader *multipart.FileHeader) error {
const maxSize = 5 * 1024 * 1024
if fileHeader.Size > maxSize {
return fmt.Errorf("file too large (max 5MB)")
}
contentType := fileHeader.Header.Get("Content-Type")
allowedTypes := []string{"image/jpeg", "image/png", "image/gif"}
validType := false
for _, t := range allowedTypes {
if contentType == t {
validType = true
break
}
}
if !validType {
return fmt.Errorf("invalid file type: %s", contentType)
}
ext := strings.ToLower(filepath.Ext(fileHeader.Filename))
allowedExts := []string{".jpg", ".jpeg", ".png", ".gif"}
validExt := false
for _, e := range allowedExts {
if ext == e {
validExt = true
break
}
}
if !validExt {
return fmt.Errorf("invalid file extension: %s", ext)
}
if strings.Contains(fileHeader.Filename, "..") || strings.Contains(fileHeader.Filename, "/") {
return fmt.Errorf("invalid filename")
}
return nil
}
func UploadFile(c *gin.Context) {
file, err := c.FormFile("file")
if err != nil {
c.JSON(400, gin.H{"error": "No file uploaded"})
return
}
if err := validateFileUpload(file); err != nil {
c.JSON(400, gin.H{"error": err.Error()})
return
}
safeFilename := generateSafeFilename(file.Filename)
dst := filepath.Join("uploads", safeFilename)
if err := c.SaveUploadedFile(file, dst); err != nil {
c.JSON(500, gin.H{"error": "Failed to save file"})
return
}
c.JSON(200, gin.H{"message": "File uploaded successfully"})
}
Verification Steps
3. SQL Injection Prevention
❌ NEVER Concatenate SQL
func getUserByEmailUnsafe(email string) (*User, error) {
query := fmt.Sprintf("SELECT * FROM users WHERE email = '%s'", email)
var user User
err := db.Raw(query).Scan(&user).Error
return &user, err
}
func searchUsersUnsafe(search string) ([]User, error) {
condition := ""
if search != "" {
condition = fmt.Sprintf("WHERE name LIKE '%%%s%%'", search)
}
query := fmt.Sprintf("SELECT * FROM users %s", condition)
var users []User
err := db.Raw(query).Scan(&users).Error
return users, err
}
✅ ALWAYS Use Parameterized Queries
func getUserByEmailSafe(email string) (*User, error) {
var user User
err := db.Where("email = ?", email).First(&user).Error
return &user, err
}
func searchUsersSafe(search string) ([]User, error) {
dbQuery := db.Model(&User{})
if search != "" {
dbQuery = dbQuery.Where("name LIKE ?", "%"+search+"%")
}
var users []User
err := dbQuery.Find(&users).Error
return users, err
}
func getUserRawSafe(email string) (*User, error) {
var user User
err := db.Raw("SELECT * FROM users WHERE email = ?", email).Scan(&user).Error
return &user, err
}
func getUsersByIDs(ids []uint) ([]User, error) {
var users []User
query := "SELECT * FROM users WHERE id IN (?)"
err := db.Raw(query, ids).Scan(&users).Error
return users, err
}
func updateUserEmail(userID uint, newEmail string) error {
result := db.Exec("UPDATE users SET email = ? WHERE id = ?", newEmail, userID)
return result.Error
}
Verification Steps
4. Authentication & Authorization
JWT Token Handling and Storage
func LoginHandler(c *gin.Context) {
token, err := generateJWT(user)
if err != nil {
c.JSON(500, gin.H{"error": "Failed to generate token"})
return
}
c.JSON(200, gin.H{"token": token})
}
func LoginHandlerSecure(c *gin.Context) {
token, err := generateJWT(user)
if err != nil {
c.JSON(500, gin.H{"error": "Failed to generate token"})
return
}
c.SetCookie(
"token",
token,
3600,
"/",
".example.com",
true,
true,
)
c.JSON(200, gin.H{"message": "Login successful"})
}
import "github.com/golang-jwt/jwt/v4"
type Claims struct {
UserID uint `json:"user_id"`
Role string `json:"role"`
jwt.RegisteredClaims
}
func generateJWT(user User) (string, error) {
claims := &Claims{
UserID: user.ID,
Role: user.Role,
RegisteredClaims: jwt.RegisteredClaims{
ExpiresAt: jwt.NewNumericDate(time.Now().Add(24 * time.Hour)),
IssuedAt: jwt.NewNumericDate(time.Now()),
Issuer: "myapp",
},
}
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
return token.SignedString([]byte(os.Getenv("JWT_SECRET")))
}
func AuthMiddleware() gin.HandlerFunc {
return func(c *gin.Context) {
tokenString, err := c.Cookie("token")
if err != nil {
authHeader := c.GetHeader("Authorization")
if authHeader == "" {
c.JSON(401, gin.H{"error": "Authorization required"})
c.Abort()
return
}
parts := strings.Split(authHeader, " ")
if len(parts) != 2 || parts[0] != "Bearer" {
c.JSON(401, gin.H{"error": "Invalid authorization header"})
c.Abort()
return
}
tokenString = parts[1]
}
token, err := jwt.ParseWithClaims(tokenString, &Claims{}, 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 []byte(os.Getenv("JWT_SECRET")), nil
})
if err != nil || !token.Valid {
c.JSON(401, gin.H{"error": "Invalid or expired token"})
c.Abort()
return
}
claims, ok := token.Claims.(*Claims)
if !ok {
c.JSON(401, gin.H{"error": "Invalid token claims"})
c.Abort()
return
}
c.Set("userID", claims.UserID)
c.Set("userRole", claims.Role)
c.Next()
}
}
Authorization Checks
func RequireRole(requiredRole string) gin.HandlerFunc {
return func(c *gin.Context) {
userRole, exists := c.Get("userRole")
if !exists {
c.JSON(500, gin.H{"error": "User role not found in context"})
c.Abort()
return
}
if userRole != requiredRole {
c.JSON(403, gin.H{"error": "Insufficient permissions"})
c.Abort()
return
}
c.Next()
}
}
func DeleteUser(userID uint, requesterID uint) error {
var requester User
if err := db.First(&requester, requesterID).Error; err != nil {
return fmt.Errorf("requester not found: %w", err)
}
if requester.Role != "admin" {
return fmt.Errorf("unauthorized: admin role required")
}
if requesterID == userID {
return fmt.Errorf("cannot delete your own account")
}
if err := db.Delete(&User{}, userID).Error; err != nil {
return fmt.Errorf("failed to delete user: %w", err)
}
return nil
}
func DeleteUserHandler(c *gin.Context) {
userIDStr := c.Param("id")
userID, err := strconv.ParseUint(userIDStr, 10, 32)
if err != nil {
c.JSON(400, gin.H{"error": "Invalid user ID"})
return
}
requesterID, exists := c.Get("userID")
if !exists {
c.JSON(500, gin.H{"error": "User ID not found in context"})
return
}
if err := DeleteUser(uint(userID), requesterID.(uint)); err != nil {
if strings.Contains(err.Error(), "unauthorized") {
c.JSON(403, gin.H{"error": err.Error()})
} else {
c.JSON(500, gin.H{"error": "Internal server error"})
}
return
}
c.JSON(200, gin.H{"message": "User deleted successfully"})
}
Row Level Security (Application-Level)
func UserScope(userID uint) func(db *gorm.DB) *gorm.DB {
return func(db *gorm.DB) *gorm.DB {
return db.Where("user_id = ?", userID)
}
}
func GetUserOrders(c *gin.Context) {
userID, exists := c.Get("userID")
if !exists {
c.JSON(500, gin.H{"error": "User ID not found in context"})
return
}
var orders []Order
err := db.Scopes(UserScope(userID.(uint))).Find(&orders).Error
if err != nil {
c.JSON(500, gin.H{"error": "Failed to fetch orders"})
return
}
c.JSON(200, gin.H{"data": orders})
}
type UserRepository struct {
db *gorm.DB
currentUserID uint
}
func (r *UserRepository) FindAll() ([]User, error) {
var users []User
if r.isAdmin() {
err := r.db.Find(&users).Error
return users, err
} else {
err := r.db.Where("id = ?", r.currentUserID).Find(&users).Error
return users, err
}
}
func (r *UserRepository) isAdmin() bool {
return false
}
Verification Steps
5. XSS Prevention
HTML Template Auto-Escaping
import (
"html/template"
"net/http"
)
func UserProfileHandler(w http.ResponseWriter, r *http.Request) {
user := getUserFromRequest(r)
tmpl := `
<!DOCTYPE html>
<html>
<head><title>User Profile</title></head>
<body>
<h1>User Profile</h1>
<p>Name: {{.Name}}</p> <!-- Auto-escaped -->
<p>Bio: {{.Bio}}</p> <!-- Auto-escaped -->
<p>Email: {{.Email}}</p> <!-- Auto-escaped -->
</body>
</html>
`
t, err := template.New("profile").Parse(tmpl)
if err != nil {
http.Error(w, "Template error", http.StatusInternalServerError)
return
}
err = t.Execute(w, user)
if err != nil {
http.Error(w, "Template execution error", http.StatusInternalServerError)
}
}
func RenderSafeHTML(w http.ResponseWriter, r *http.Request) {
userContent := getUserContent()
tmpl := `
<!DOCTYPE html>
<html>
<body>
<div class="user-content">
<!-- Use template.HTML type to mark content as safe -->
{{.SafeContent}}
</div>
</body>
</html>
`
t, err := template.New("content").Parse(tmpl)
if err != nil {
http.Error(w, "Template error", http.StatusInternalServerError)
return
}
data := struct {
SafeContent template.HTML
}{
SafeContent: template.HTML(sanitizeHTML(userContent)),
}
err = t.Execute(w, data)
if err != nil {
http.Error(w, "Template execution error", http.StatusInternalServerError)
}
}
HTML Sanitization for User Content
import (
"github.com/microcosm-cc/bluemonday"
)
func sanitizeHTML(input string) string {
p := bluemonday.StrictPolicy()
p = bluemonday.UGCPolicy()
p.AllowAttrs("class").OnElements("span", "div")
p.AllowElements("br", "hr")
return p.Sanitize(input)
}
func SubmitCommentHandler(c *gin.Context) {
var req struct {
Content string `json:"content" binding:"required"`
}
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(400, gin.H{"error": "Invalid input"})
return
}
sanitizedContent := sanitizeHTML(req.Content)
comment := Comment{
Content: sanitizedContent,
UserID: getUserID(c),
}
if err := db.Create(&comment).Error; err != nil {
c.JSON(500, gin.H{"error": "Failed to save comment"})
return
}
c.JSON(200, gin.H{"message": "Comment submitted"})
}
Content Security Policy Headers
func SecurityHeaders() gin.HandlerFunc {
return func(c *gin.Context) {
c.Header("Content-Security-Policy",
"default-src 'self'; " +
"script-src 'self' 'unsafe-inline' https://cdn.example.com; " +
"style-src 'self' 'unsafe-inline' https://fonts.googleapis.com; " +
"img-src 'self' data: https:; " +
"font-src 'self' https://fonts.gstatic.com; " +
"connect-src 'self' https://api.example.com; " +
"frame-ancestors 'none'; " +
"form-action 'self';")
c.Header("X-Content-Type-Options", "nosniff")
c.Header("X-Frame-Options", "DENY")
c.Header("X-XSS-Protection", "1; mode=block")
c.Header("Referrer-Policy", "strict-origin-when-cross-origin")
c.Header("Permissions-Policy", "geolocation=(), microphone=(), camera=()")
c.Next()
}
}
func main() {
r := gin.Default()
r.Use(SecurityHeaders())
r.Run(":8080")
}
Verification Steps
6. CSRF Protection
CSRF Token Implementation
import (
"crypto/rand"
"encoding/base64"
"github.com/gin-gonic/gin"
"time"
)
func generateCSRFToken() (string, error) {
bytes := make([]byte, 32)
if _, err := rand.Read(bytes); err != nil {
return "", err
}
return base64.URLEncoding.EncodeToString(bytes), nil
}
func CSRFMiddleware() gin.HandlerFunc {
return func(c *gin.Context) {
if c.Request.Method == "GET" || c.Request.Method == "HEAD" || c.Request.Method == "OPTIONS" {
c.Next()
return
}
token := c.GetHeader("X-CSRF-Token")
if token == "" {
token = c.PostForm("csrf_token")
}
expectedToken, err := c.Cookie("csrf_token")
if err != nil || expectedToken == "" {
c.JSON(403, gin.H{"error": "CSRF token missing from session"})
c.Abort()
return
}
if token != expectedToken {
c.JSON(403, gin.H{"error": "Invalid CSRF token"})
c.Abort()
return
}
c.Next()
}
}
func GetCSRFToken(c *gin.Context) {
token, err := generateCSRFToken()
if err != nil {
c.JSON(500, gin.H{"error": "Failed to generate CSRF token"})
return
}
c.SetCookie(
"csrf_token",
token,
3600,
"/",
"",
true,
true,
)
c.JSON(200, gin.H{"csrf_token": token})
}
func ChangePasswordHandler(c *gin.Context) {
var req struct {
NewPassword string `json:"new_password" binding:"required,min=8"`
}
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(400, gin.H{"error": "Invalid input"})
return
}
c.JSON(200, gin.H{"message": "Password changed successfully"})
}
SameSite Cookie Configuration
func SetAuthCookie(c *gin.Context, token string) {
c.SetCookie(
"session",
token,
24*3600,
"/",
".example.com",
true,
true,
)
}
import "net/http"
func SetSecureCookie(w http.ResponseWriter, name, value string) {
cookie := &http.Cookie{
Name: name,
Value: value,
Path: "/",
Domain: ".example.com",
MaxAge: 3600,
Secure: true,
HttpOnly: true,
SameSite: http.SameSiteStrictMode,
}
http.SetCookie(w, cookie)
}
func SetSecureCookieGin(c *gin.Context, name, value string) {
cookie := &http.Cookie{
Name: name,
Value: value,
Path: "/",
Domain: ".example.com",
MaxAge: 3600,
Secure: true,
HttpOnly: true,
SameSite: http.SameSiteStrictMode,
}
http.SetCookie(c.Writer, cookie)
}
Double-Submit Cookie Pattern
func SetupCSRFDoubleSubmit() gin.HandlerFunc {
return func(c *gin.Context) {
if c.Request.Method == "GET" || c.Request.Method == "HEAD" || c.Request.Method == "OPTIONS" {
token, _ := generateCSRFToken()
c.SetCookie(
"csrf_token",
token,
3600,
"/",
"",
true,
false,
)
c.Next()
return
}
headerToken := c.GetHeader("X-CSRF-Token")
cookieToken, _ := c.Cookie("csrf_token")
if headerToken == "" || cookieToken == "" || headerToken != cookieToken {
c.JSON(403, gin.H{"error": "Invalid CSRF token"})
c.Abort()
return
}
c.Next()
}
}
Verification Steps
7. Rate Limiting
Basic Rate Limiting Middleware
import (
"github.com/gin-gonic/gin"
"sync"
"time"
)
type RateLimiter struct {
requests map[string][]time.Time
mu sync.RWMutex
limit int
window time.Duration
}
func NewRateLimiter(limit int, window time.Duration) *RateLimiter {
return &RateLimiter{
requests: make(map[string][]time.Time),
limit: limit,
window: window,
}
}
func (rl *RateLimiter) Allow(identifier string) bool {
rl.mu.Lock()
defer rl.mu.Unlock()
now := time.Now()
cutoff := now.Add(-rl.window)
requests := rl.requests[identifier]
var validRequests []time.Time
for _, t := range requests {
if t.After(cutoff) {
validRequests = append(validRequests, t)
}
}
if len(validRequests) >= rl.limit {
return false
}
validRequests = append(validRequests, now)
rl.requests[identifier] = validRequests
return true
}
func RateLimitMiddleware(limiter *RateLimiter) gin.HandlerFunc {
return func(c *gin.Context) {
identifier := c.ClientIP()
if !limiter.Allow(identifier) {
c.Header("Retry-After", limiter.window.String())
c.JSON(429, gin.H{
"error": "Too many requests",
"retry_after": limiter.window.Seconds(),
})
c.Abort()
return
}
c.Next()
}
}
func main() {
r := gin.Default()
globalLimiter := NewRateLimiter(100, 15*time.Minute)
r.Use(RateLimitMiddleware(globalLimiter))
loginLimiter := NewRateLimiter(10, 5*time.Minute)
r.POST("/login", RateLimitMiddleware(loginLimiter), LoginHandler)
r.Run(":8080")
}
Using ulule/limiter Package (Recommended)
import (
"github.com/ulule/limiter/v3"
"github.com/ulule/limiter/v3/drivers/store/memory"
limitergin "github.com/ulule/limiter/v3/drivers/middleware/gin"
"github.com/gin-gonic/gin"
"time"
)
func setupRateLimiting() {
globalRate := limiter.Rate{
Period: 15 * time.Minute,
Limit: 100,
}
loginRate := limiter.Rate{
Period: 5 * time.Minute,
Limit: 10,
}
searchRate := limiter.Rate{
Period: 1 * time.Minute,
Limit: 30,
}
globalStore := memory.NewStore()
loginStore := memory.NewStore()
searchStore := memory.NewStore()
globalLimiter := limiter.New(globalStore, globalRate)
loginLimiter := limiter.New(loginStore, loginRate)
searchLimiter := limiter.New(searchStore, searchRate)
globalMiddleware := limitergin.NewMiddleware(globalLimiter)
loginMiddleware := limitergin.NewMiddleware(loginLimiter)
searchMiddleware := limitergin.NewMiddleware(searchLimiter)
r := gin.Default()
r.Use(globalMiddleware)
r.POST("/login", loginMiddleware, LoginHandler)
r.POST("/register", loginMiddleware, RegisterHandler)
r.GET("/search", searchMiddleware, SearchHandler)
}
User-Based Rate Limiting
func UserRateLimitMiddleware(limiter *RateLimiter) gin.HandlerFunc {
return func(c *gin.Context) {
var identifier string
userID, exists := c.Get("userID")
if exists {
identifier = fmt.Sprintf("user:%v", userID)
} else {
identifier = fmt.Sprintf("ip:%s", c.ClientIP())
}
if !limiter.Allow(identifier) {
c.Header("Retry-After", limiter.window.String())
c.JSON(429, gin.H{
"error": "Too many requests",
"retry_after": limiter.window.Seconds(),
})
c.Abort()
return
}
c.Next()
}
}
func RoleBasedRateLimitMiddleware() gin.HandlerFunc {
return func(c *gin.Context) {
userRole, exists := c.Get("userRole")
var limiter *RateLimiter
if exists && userRole == "admin" {
limiter = NewRateLimiter(1000, time.Minute)
} else if exists {
limiter = NewRateLimiter(100, time.Minute)
} else {
limiter = NewRateLimiter(10, time.Minute)
}
var identifier string
if userID, exists := c.Get("userID"); exists {
identifier = fmt.Sprintf("user:%v", userID)
} else {
identifier = c.ClientIP()
}
if !limiter.Allow(identifier) {
c.Header("Retry-After", limiter.window.String())
c.JSON(429, gin.H{"error": "Too many requests"})
c.Abort()
return
}
c.Next()
}
}
Verification Steps
8. Sensitive Data Exposure
Logging
log.Printf("User login: email=%s, password=%s", email, password)
slog.Info("Payment processing", "card_number", cardNumber, "cvv", cvv)
log.Printf("User login attempt: email=%s", email)
slog.Info("Payment processed", "user_id", userID, "last4", last4Digits)
logger := slog.New(slog.NewJSONHandler(os.Stdout, nil))
logger.Info("User authenticated",
"user_id", user.ID,
"email_hash", hashEmail(user.Email),
"action", "login",
)
func hashEmail(email string) string {
h := sha256.New()
h.Write([]byte(email))
return hex.EncodeToString(h.Sum(nil))[:8]
}
Error Messages
func getUserHandler(c *gin.Context) {
userID := c.Param("id")
user, err := service.GetUser(userID)
if err != nil {
c.JSON(500, gin.H{
"error": fmt.Sprintf("Database error: %v", err),
"query": "SELECT * FROM users WHERE id = ?",
})
return
}
c.JSON(200, user)
}
func getUserHandlerSecure(c *gin.Context) {
userID := c.Param("id")
user, err := service.GetUser(userID)
if err != nil {
slog.Error("Failed to get user",
"user_id", userID,
"error", err,
"trace_id", getTraceID(c),
)
if errors.Is(err, gorm.ErrRecordNotFound) {
c.JSON(404, gin.H{"error": "用户不存在"})
} else {
c.JSON(500, gin.H{"error": "内部服务器错误"})
}
return
}
c.JSON(200, user)
}
type AppError struct {
Code int `json:"code"`
Message string `json:"message"`
Err error `json:"-"`
}
func (e *AppError) Error() string {
return e.Message
}
func NewAppError(code int, message string, err error) *AppError {
return &AppError{
Code: code,
Message: message,
Err: err,
}
}
func processPayment(c *gin.Context) {
if err := service.ProcessPayment(); err != nil {
var appErr *AppError
if errors.As(err, &appErr) {
slog.Error("Payment processing failed",
"error", appErr.Err,
"user_id", getUserID(c),
"payment_id", getPaymentID(c),
)
c.JSON(appErr.Code, gin.H{"error": appErr.Message})
} else {
slog.Error("Unknown payment error", "error", err)
c.JSON(500, gin.H{"error": "支付处理失败"})
}
return
}
c.JSON(200, gin.H{"message": "支付成功"})
}
Verification Steps
9. Cryptography and Key Management
Secure Key Storage and Usage
const encryptionKey = "my-secret-key-12345"
import (
"crypto/aes"
"crypto/cipher"
"crypto/rand"
"encoding/base64"
"io"
)
func getEncryptionKey() ([]byte, error) {
keyBase64 := os.Getenv("ENCRYPTION_KEY")
if keyBase64 == "" {
return nil, fmt.Errorf("ENCRYPTION_KEY environment variable not set")
}
key, err := base64.StdEncoding.DecodeString(keyBase64)
if err != nil {
return nil, fmt.Errorf("failed to decode encryption key: %w", err)
}
if len(key) != 32 {
return nil, fmt.Errorf("encryption key must be 32 bytes for AES-256")
}
return key, nil
}
func encryptData(plaintext []byte) ([]byte, error) {
key, err := getEncryptionKey()
if err != nil {
return nil, err
}
block, err := aes.NewCipher(key)
if err != nil {
return nil, err
}
gcm, err := cipher.NewGCM(block)
if err != nil {
return nil, err
}
nonce := make([]byte, gcm.NonceSize())
if _, err := io.ReadFull(rand.Reader, nonce); err != nil {
return nil, err
}
ciphertext := gcm.Seal(nonce, nonce, plaintext, nil)
return ciphertext, nil
}
func decryptData(ciphertext []byte) ([]byte, error) {
key, err := getEncryptionKey()
if err != nil {
return nil, err
}
block, err := aes.NewCipher(key)
if err != nil {
return nil, err
}
gcm, err := cipher.NewGCM(block)
if err != nil {
return nil, err
}
nonceSize := gcm.NonceSize()
if len(ciphertext) < nonceSize {
return nil, fmt.Errorf("ciphertext too short")
}
nonce, ciphertext := ciphertext[:nonceSize], ciphertext[nonceSize:]
plaintext, err := gcm.Open(nil, nonce, ciphertext, nil)
if err != nil {
return nil, err
}
return plaintext, nil
}
Password Hashing with Argon2
import "crypto/sha256"
func hashPasswordWeak(password string) string {
hash := sha256.Sum256([]byte(password))
return hex.EncodeToString(hash[:])
}
import "golang.org/x/crypto/argon2"
import "golang.org/x/crypto/bcrypt"
func hashPasswordBcrypt(password string) (string, error) {
hashedPassword, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
if err != nil {
return "", fmt.Errorf("failed to hash password: %w", err)
}
return string(hashedPassword), nil
}
func verifyPasswordBcrypt(hashedPassword, password string) bool {
err := bcrypt.CompareHashAndPassword([]byte(hashedPassword), []byte(password))
return err == nil
}
type Argon2Params struct {
Memory uint32
Iterations uint32
Parallelism uint8
SaltLength uint32
KeyLength uint32
}
var DefaultArgon2Params = &Argon2Params{
Memory: 64 * 1024,
Iterations: 3,
Parallelism: 2,
SaltLength: 16,
KeyLength: 32,
}
func hashPasswordArgon2(password string, p *Argon2Params) (string, error) {
salt := make([]byte, p.SaltLength)
if _, err := rand.Read(salt); err != nil {
return "", err
}
hash := argon2.IDKey([]byte(password), salt, p.Iterations, p.Memory, p.Parallelism, p.KeyLength)
encodedHash := base64.RawStdEncoding.EncodeToString(hash)
encodedSalt := base64.RawStdEncoding.EncodeToString(salt)
return fmt.Sprintf("%s:%s", encodedHash, encodedSalt), nil
}
func verifyPasswordArgon2(password, encodedHash string, p *Argon2Params) (bool, error) {
parts := strings.Split(encodedHash, ":")
if len(parts) != 2 {
return false, fmt.Errorf("invalid encoded hash format")
}
hash, err := base64.RawStdEncoding.DecodeString(parts[0])
if err != nil {
return false, err
}
salt, err := base64.RawStdEncoding.DecodeString(parts[1])
if err != nil {
return false, err
}
computedHash := argon2.IDKey([]byte(password), salt, p.Iterations, p.Memory, p.Parallelism, p.KeyLength)
return subtle.ConstantTimeCompare(hash, computedHash) == 1, nil
}
Digital Signatures and Verification
import (
"crypto/ed25519"
"crypto/rand"
)
func generateKeyPair() (ed25519.PublicKey, ed25519.PrivateKey, error) {
publicKey, privateKey, err := ed25519.GenerateKey(rand.Reader)
if err != nil {
return nil, nil, err
}
return publicKey, privateKey, nil
}
func signMessage(privateKey ed25519.PrivateKey, message []byte) []byte {
signature := ed25519.Sign(privateKey, message)
return signature
}
func verifySignature(publicKey ed25519.PublicKey, message, signature []byte) bool {
return ed25519.Verify(publicKey, message, signature)
}
func signAPIRequest(privateKey ed25519.PrivateKey, method, path, body string, timestamp int64) (string, error) {
message := fmt.Sprintf("%s\n%s\n%s\n%d", method, path, body, timestamp)
signature := signMessage(privateKey, []byte(message))
return base64.StdEncoding.EncodeToString(signature), nil
}
func verifyAPIRequest(publicKey ed25519.PublicKey, method, path, body string, timestamp int64, signatureBase64 string) bool {
signature, err := base64.StdEncoding.DecodeString(signatureBase64)
if err != nil {
return false
}
message := fmt.Sprintf("%s\n%s\n%s\n%d", method, path, body, timestamp)
return verifySignature(publicKey, []byte(message), signature)
}
Verification Steps
10. Go Dependency Security
Regular Updates and Vulnerability Scanning
go list -m -u all
go get -u ./...
go get -u github.com/gin-gonic/gin
go install golang.org/x/vuln/cmd/govulncheck@latest
govulncheck ./...
go install github.com/securego/gosec/v2/cmd/gosec@latest
gosec ./...
go list -m -f '{{if not .Indirect}}{{.Path}} {{.Version}}{{end}}' all
go mod tidy
go mod verify
go.mod and go.sum Files
git add go.mod go.sum
go mod vendor
go mod download
go build -mod=readonly ./...
go build -mod=vendor ./...
go mod verify
Dependency Analysis Tools
package main
import (
"fmt"
"golang.org/x/mod/modfile"
"io/ioutil"
"os"
)
func checkDependencies() error {
data, err := ioutil.ReadFile("go.mod")
if err != nil {
return fmt.Errorf("failed to read go.mod: %w", err)
}
f, err := modfile.Parse("go.mod", data, nil)
if err != nil {
return fmt.Errorf("failed to parse go.mod: %w", err)
}
fmt.Println("Direct dependencies:")
for _, req := range f.Require {
if !req.Indirect {
fmt.Printf(" %s %s\n", req.Mod.Path, req.Mod.Version)
}
}
fmt.Println("\nIndirect dependencies:")
for _, req := range f.Require {
if req.Indirect {
fmt.Printf(" %s %s\n", req.Mod.Path, req.Mod.Version)
}
}
return nil
}
import "golang.org/x/tools/go/packages"
func analyzePackageImports() error {
cfg := &packages.Config{
Mode: packages.NeedImports | packages.NeedDeps,
}
pkgs, err := packages.Load(cfg, "./...")
if err != nil {
return fmt.Errorf("failed to load packages: %w", err)
}
for _, pkg := range pkgs {
fmt.Printf("Package: %s\n", pkg.ID)
for _, imp := range pkg.Imports {
fmt.Printf(" Import: %s\n", imp.ID)
}
}
return nil
}
Security Scanning in CI/CD
name: Go Security Scan
on:
push:
branches: [ main ]
pull_request:
branches: [ main ]
jobs:
security-scan:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Set up Go
uses: actions/setup-go@v4
with:
go-version: '1.21'
- name: Run govulncheck
run: |
go install golang.org/x/vuln/cmd/govulncheck@latest
govulncheck ./...
- name: Run gosec
run: |
go install github.com/securego/gosec/v2/cmd/gosec@latest
gosec ./...
- name: Check for outdated dependencies
run: |
go list -m -u all | grep -E "\[.*\]"
- name: Run go mod tidy and verify
run: |
go mod tidy
go mod verify
git diff --exit-code go.mod go.sum
Verification Steps
Security Testing for Go Applications
Automated Security Tests with Go Testing
func TestAuthMiddleware(t *testing.T) {
r := gin.New()
r.Use(AuthMiddleware())
r.GET("/protected", func(c *gin.Context) {
c.JSON(200, gin.H{"message": "protected"})
})
req := httptest.NewRequest("GET", "/protected", nil)
w := httptest.NewRecorder()
r.ServeHTTP(w, req)
assert.Equal(t, http.StatusUnauthorized, w.Code)
assert.Contains(t, w.Body.String(), "Authorization required")
req = httptest.NewRequest("GET", "/protected", nil)
req.Header.Set("Authorization", "Bearer invalid-token")
w = httptest.NewRecorder()
r.ServeHTTP(w, req)
assert.Equal(t, http.StatusUnauthorized, w.Code)
assert.Contains(t, w.Body.String(), "Invalid or expired token")
}
func TestRequireAdminRole(t *testing.T) {
r := gin.New()
r.GET("/admin", func(c *gin.Context) {
c.Set("userRole", "user")
RequireRole("admin")(c)
if c.IsAborted() {
return
}
c.JSON(200, gin.H{"message": "admin access"})
})
req := httptest.NewRequest("GET", "/admin", nil)
w := httptest.NewRecorder()
r.ServeHTTP(w, req)
assert.Equal(t, http.StatusForbidden, w.Code)
assert.Contains(t, w.Body.String(), "Insufficient permissions")
}
func TestInputValidation(t *testing.T) {
r := gin.New()
r.POST("/users", CreateUserHandler)
jsonStr := `{"email": "not-an-email", "name": "test", "age": 25, "password": "password123"}`
req := httptest.NewRequest("POST", "/users", strings.NewReader(jsonStr))
req.Header.Set("Content-Type", "application/json")
w := httptest.NewRecorder()
r.ServeHTTP(w, req)
assert.Equal(t, http.StatusBadRequest, w.Code)
assert.Contains(t, w.Body.String(), "Invalid input")
jsonStr = `{"email": "test@example.com", "age": 25}`
req = httptest.NewRequest("POST", "/users", strings.NewReader(jsonStr))
req.Header.Set("Content-Type", "application/json")
w = httptest.NewRecorder()
r.ServeHTTP(w, req)
assert.Equal(t, http.StatusBadRequest, w.Code)
}
func TestSQLInjectionPrevention(t *testing.T) {
db, mock, err := sqlmock.New()
require.NoError(t, err)
defer db.Close()
mock.ExpectQuery("SELECT \\* FROM users WHERE email = \\?").
WithArgs("test@example.com").
WillReturnRows(sqlmock.NewRows([]string{"id", "email"}))
_, err = getUserByEmailSafe(db, "test@example.com")
assert.NoError(t, err)
assert.NoError(t, mock.ExpectationsWereMet())
}
func TestRateLimiting(t *testing.T) {
limiter := NewRateLimiter(5, time.Minute)
identifier := "test-ip"
for i := 0; i < 5; i++ {
assert.True(t, limiter.Allow(identifier), fmt.Sprintf("Request %d should be allowed", i+1))
}
assert.False(t, limiter.Allow(identifier), "6th request should be blocked")
assert.True(t, limiter.Allow("different-ip"), "Different IP should be allowed")
}
func TestCSRFProtection(t *testing.T) {
r := gin.New()
r.Use(CSRFMiddleware())
r.POST("/change-password", ChangePasswordHandler)
req := httptest.NewRequest("POST", "/change-password", nil)
w := httptest.NewRecorder()
r.ServeHTTP(w, req)
assert.Equal(t, http.StatusForbidden, w.Code)
assert.Contains(t, w.Body.String(), "CSRF token")
}
func TestXSSPrevention(t *testing.T) {
tmpl := `{{.UserInput}}`
tpl, err := template.New("test").Parse(tmpl)
require.NoError(t, err)
var buf bytes.Buffer
data := map[string]interface{}{
"UserInput": `<script>alert('xss')</script>`,
}
err = tpl.Execute(&buf, data)
require.NoError(t, err)
output := buf.String()
assert.Contains(t, output, "<script>")
assert.Contains(t, output, "</script>")
assert.NotContains(t, output, "<script>")
}
func TestFileUploadValidation(t *testing.T) {
fileHeader := &multipart.FileHeader{
Filename: "test.jpg",
Size: 1024 * 1024,
Header: map[string][]string{"Content-Type": {"image/jpeg"}},
}
err := validateFileUpload(fileHeader)
assert.NoError(t, err)
fileHeader.Size = 10 * 1024 * 1024
err = validateFileUpload(fileHeader)
assert.Error(t, err)
assert.Contains(t, err.Error(), "too large")
fileHeader.Size = 1024 * 1024
fileHeader.Header["Content-Type"] = []string{"application/exe"}
err = validateFileUpload(fileHeader)
assert.Error(t, err)
assert.Contains(t, err.Error(), "invalid file type")
fileHeader.Header["Content-Type"] = []string{"image/jpeg"}
fileHeader.Filename = "../../etc/passwd"
err = validateFileUpload(fileHeader)
assert.Error(t, err)
assert.Contains(t, err.Error(), "invalid filename")
}
func TestPasswordHashing(t *testing.T) {
password := "mySecurePassword123"
hashed, err := hashPasswordBcrypt(password)
require.NoError(t, err)
assert.True(t, verifyPasswordBcrypt(hashed, password))
assert.False(t, verifyPasswordBcrypt(hashed, "wrongpassword"))
params := &Argon2Params{
Memory: 64 * 1024,
Iterations: 3,
Parallelism: 2,
SaltLength: 16,
KeyLength: 32,
}
hashed, err = hashPasswordArgon2(password, params)
require.NoError(t, err)
valid, err := verifyPasswordArgon2(password, hashed, params)
require.NoError(t, err)
assert.True(t, valid)
}
Pre-Deployment Security Checklist for Go Applications
Before ANY production deployment of Go web applications:
Authentication & Authorization
Input & Data Security
API & Network Security
Application Security
Dependency & Configuration Security
Infrastructure & Deployment Security
Go-Specific Security
Compliance & Verification
Go Security Resources
Official Documentation
OWASP & Web Security
Go Security Tools
Gin Framework Security
Database Security
Cryptography Resources
Books & Articles
Remember: Security is not optional. One vulnerability can compromise the entire platform. When in doubt, err on the side of caution.