🔐 Security✍️ Khoa📅 19/04/2026☕ 12 phút đọc

Secure Coding Practices

Secure coding không phải là checklist mà apply một lần rồi quên. Nó là mindset — cách bạn suy nghĩ về code từ khi viết dòng đầu tiên. Section này cover các best practices để tránh vulnerabilities phổ biến nhất.

🛡️ Nguyên tắc vàng: "Code an toàn không phải là code không có bugs, mà là code mà bugs trong đó không exploitable."


Input Validation: First Line of Defense

Rule #1: Never trust user input.
Rule #2: NEVER trust user input.
Rule #3: See Rule #1.

Whitelist vs Blacklist

// ❌ BLACKLIST approach (BAD)
func validateUsername(username string) bool {
    forbidden := []string{"admin", "root", "system"}
    for _, bad := range forbidden {
        if username == bad {
            return false
        }
    }
    return true
    // What about "Admin", "ADMIN", "admin1"?
}

// ✅ WHITELIST approach (GOOD)
func validateUsername(username string) bool {
    // Only allow alphanumeric + underscore, 3-20 chars
    match, _ := regexp.MatchString(`^[a-zA-Z0-9_]{3,20}


  
  
  
  Secure Coding Practices — Khoa's Blog
  
  
  
  
  


  
  

  
, username) return match }

Type-based Validation

// Validate email
func isValidEmail(email string) bool {
    pattern := `^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}


  
  
  
  Secure Coding Practices — Khoa's Blog
  
  
  
  
  


  
  

  
match, _ := regexp.MatchString(pattern, email) return match } // Validate URL func isValidURL(urlStr string) bool { u, err := url.Parse(urlStr) return err == nil && u.Scheme != "" && u.Host != "" } // Validate integer range func isValidAge(age int) bool { return age >= 0 && age <= 150 } // Sanitize filename (prevent path traversal) func sanitizeFilename(filename string) string { // Remove path separators filename = strings.ReplaceAll(filename, "/", "") filename = strings.ReplaceAll(filename, "\\", "") filename = strings.ReplaceAll(filename, "..", "") // Whitelist approach: only allow alphanumeric + dot + dash safe := regexp.MustCompile(`[^a-zA-Z0-9._-]`) return safe.ReplaceAllString(filename, "_") }

Context-aware Validation

Input validation phụ thuộc vào context — input hợp lệ cho HTML có thể không hợp lệ cho SQL.

type UserInput struct {
    Name    string
    Email   string
    Bio     string // HTML allowed
    Age     int
}

func validateUserInput(input UserInput) error {
    // Name: alphanumeric + spaces only
    if !regexp.MustCompile(`^[a-zA-Z\s]{1,100}


  
  
  
  Secure Coding Practices — Khoa's Blog
  
  
  
  
  


  
  

  
).MatchString(input.Name) { return errors.New("invalid name") } // Email: proper format if !isValidEmail(input.Email) { return errors.New("invalid email") } // Bio: HTML tags allowed, but sanitize input.Bio = sanitizeHTML(input.Bio) // Age: reasonable range if input.Age < 0 || input.Age > 150 { return errors.New("invalid age") } return nil }

Output Encoding: Prevent Injection Attacks

HTML Encoding (Prevent XSS)

import "html/template"

// ❌ VULNERABLE to XSS
func renderUserProfile(w http.ResponseWriter, username string) {
    html := "<h1>Welcome " + username + "</h1>"
    w.Write([]byte(html))
    // If username = "<script>alert('XSS')</script>"
    // → Executes malicious script!
}

// ✅ SAFE: Auto-escape with html/template
func renderUserProfileSafe(w http.ResponseWriter, username string) {
    tmpl := template.Must(template.New("profile").Parse("<h1>Welcome {{.}}</h1>"))
    tmpl.Execute(w, username)
    // username = "<script>alert('XSS')</script>"
    // → Rendered as: &lt;script&gt;alert(&#39;XSS&#39;)&lt;/script&gt;
}

// Manual HTML escape (if not using templates)
func escapeHTML(s string) string {
    return html.EscapeString(s)
}

JavaScript Encoding

// ❌ DANGEROUS
func renderInlineJS(w http.ResponseWriter, data string) {
    js := `<script>var message = "` + data + `";</script>`
    w.Write([]byte(js))
    // If data = `"; alert('XSS'); //`
    // → <script>var message = ""; alert('XSS'); //";</script>
}

// ✅ SAFE: JSON encode
import "encoding/json"

func renderInlineJSSafe(w http.ResponseWriter, data string) {
    jsonData, _ := json.Marshal(data)
    js := `<script>var message = ` + string(jsonData) + `;</script>`
    w.Write([]byte(js))
    // data = `"; alert('XSS'); //`
    // → <script>var message = "\"; alert('XSS'); //";</script>
}

URL Encoding

import "net/url"

// ❌ DANGEROUS
func buildURL(param string) string {
    return "https://example.com/search?q=" + param
    // If param = "foo&admin=true"
    // → https://example.com/search?q=foo&admin=true (injected param!)
}

// ✅ SAFE
func buildURLSafe(param string) string {
    return "https://example.com/search?q=" + url.QueryEscape(param)
    // param = "foo&admin=true"
    // → https://example.com/search?q=foo%26admin%3Dtrue
}

SQL Encoding (Use Parameterized Queries)

// ❌ VULNERABLE to SQL injection
func getUser(email string) (*User, error) {
    query := "SELECT * FROM users WHERE email = '" + email + "'"
    row := db.QueryRow(query)
    // ...
}

// ✅ SAFE: Parameterized query
func getUserSafe(email string) (*User, error) {
    query := "SELECT * FROM users WHERE email = ?"
    row := db.QueryRow(query, email)
    // Database driver handles escaping
}

Security Headers

HTTP headers đóng vai trò quan trọng trong defense-in-depth strategy.

Essential Security Headers

func setSecurityHeaders(w http.ResponseWriter) {
    // 1. Prevent MIME sniffing
    w.Header().Set("X-Content-Type-Options", "nosniff")

    // 2. Prevent clickjacking
    w.Header().Set("X-Frame-Options", "DENY")
    // Or: "SAMEORIGIN" to allow framing by same origin

    // 3. Enable XSS filter (legacy browsers)
    w.Header().Set("X-XSS-Protection", "1; mode=block")

    // 4. Enforce HTTPS
    w.Header().Set("Strict-Transport-Security", "max-age=31536000; includeSubDomains; preload")

    // 5. Content Security Policy (CSP)
    w.Header().Set("Content-Security-Policy", 
        "default-src 'self'; " +
        "script-src 'self' 'unsafe-inline' https://cdn.example.com; " +
        "style-src 'self' 'unsafe-inline'; " +
        "img-src 'self' data: https:; " +
        "font-src 'self'; " +
        "connect-src 'self'; " +
        "frame-ancestors 'none'")

    // 6. Referrer policy
    w.Header().Set("Referrer-Policy", "strict-origin-when-cross-origin")

    // 7. Permissions policy (formerly Feature-Policy)
    w.Header().Set("Permissions-Policy", "geolocation=(), microphone=(), camera=()")
}

// Middleware
func securityHeadersMiddleware(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        setSecurityHeaders(w)
        next.ServeHTTP(w, r)
    })
}

Content Security Policy (CSP) Deep Dive

CSP is powerful but complex. Build it incrementally:

Level 1: Report-only (observe violations without blocking)

Content-Security-Policy-Report-Only: default-src 'self'; report-uri /csp-report

Level 2: Permissive (allow what you need)

Content-Security-Policy: 
    default-src 'self';
    script-src 'self' 'unsafe-inline' https://cdn.example.com;
    style-src 'self' 'unsafe-inline';

Level 3: Strict (remove unsafe-inline using nonces)

Content-Security-Policy: 
    default-src 'self';
    script-src 'self' 'nonce-random123';
    style-src 'self';
<script nonce="random123">
    console.log('This is allowed');
</script>

CSP violations handler:

func handleCSPReport(w http.ResponseWriter, r *http.Request) {
    var report struct {
        CSPReport struct {
            DocumentURI        string `json:"document-uri"`
            BlockedURI         string `json:"blocked-uri"`
            ViolatedDirective  string `json:"violated-directive"`
            OriginalPolicy     string `json:"original-policy"`
        } `json:"csp-report"`
    }

    json.NewDecoder(r.Body).Decode(&report)
    
    // Log for analysis
    log.Printf("CSP Violation: %+v", report.CSPReport)
    
    w.WriteHeader(204) // No Content
}

CSRF Protection

Cross-Site Request Forgery: attacker tricks user into making unwanted requests.

Attack example:

<!-- Attacker's page -->
<form action="https://bank.com/transfer" method="POST">
    <input name="to" value="attacker-account" />
    <input name="amount" value="10000" />
</form>
<script>document.forms[0].submit();</script>

Nếu user đã login vào bank.com → browser tự động gửi cookies → request succeeds!

CSRF Token (Synchronizer Token Pattern)

import (
    "crypto/rand"
    "encoding/base64"
)

func generateCSRFToken() string {
    b := make([]byte, 32)
    rand.Read(b)
    return base64.URLEncoding.EncodeToString(b)
}

// Store token in session
func handleFormPage(w http.ResponseWriter, r *http.Request) {
    token := generateCSRFToken()
    
    // Store in session
    session, _ := sessionStore.Get(r, "session")
    session.Values["csrf_token"] = token
    session.Save(r, w)

    // Render form with hidden token
    tmpl := `
    <form method="POST" action="/submit">
        <input type="hidden" name="csrf_token" value="{{.Token}}" />
        <input type="text" name="data" />
        <button>Submit</button>
    </form>
    `
    template.Must(template.New("form").Parse(tmpl)).Execute(w, map[string]string{
        "Token": token,
    })
}

// Verify token on submission
func handleFormSubmit(w http.ResponseWriter, r *http.Request) {
    session, _ := sessionStore.Get(r, "session")
    expectedToken := session.Values["csrf_token"].(string)
    receivedToken := r.FormValue("csrf_token")

    if expectedToken != receivedToken {
        http.Error(w, "Invalid CSRF token", 403)
        return
    }

    // Process form...
}

// Middleware approach
func csrfMiddleware(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        if r.Method == "POST" || r.Method == "PUT" || r.Method == "DELETE" {
            session, _ := sessionStore.Get(r, "session")
            expectedToken := session.Values["csrf_token"]
            receivedToken := r.Header.Get("X-CSRF-Token")

            if expectedToken != receivedToken {
                http.Error(w, "CSRF token mismatch", 403)
                return
            }
        }
        next.ServeHTTP(w, r)
    })
}

Modern browsers support SameSite attribute → giảm CSRF risk.

http.SetCookie(w, &http.Cookie{
    Name:     "session",
    Value:    sessionID,
    HttpOnly: true,
    Secure:   true,
    SameSite: http.SameSiteStrictMode, // Block cross-site requests
    // Or: http.SameSiteLaxMode (allow top-level navigation GET)
})

Comparison:

SameSite Behavior CSRF Protection
Strict Cookie NEVER sent on cross-site requests ✅ Best
Lax Cookie sent on top-level navigation (clicking link) ✅ Good
None Cookie always sent (must use Secure) ❌ No protection

Secret Management

Rule: NEVER hardcode secrets in code.

❌ What NOT to do

// 🔥 NEVER
const dbPassword = "super_secret_123"
const apiKey = "sk_live_abc123xyz"
const jwtSecret = "my-secret-key"

✅ Environment Variables (Basic)

import "os"

var (
    dbPassword = os.Getenv("DB_PASSWORD")
    apiKey     = os.Getenv("API_KEY")
    jwtSecret  = []byte(os.Getenv("JWT_SECRET"))
)

func init() {
    if dbPassword == "" {
        log.Fatal("DB_PASSWORD not set")
    }
}

.env file (for development, add to .gitignore):

DB_PASSWORD=dev_password
API_KEY=dev_api_key
JWT_SECRET=dev_jwt_secret

✅ Secrets Manager (Production)

AWS Secrets Manager:

import (
    "github.com/aws/aws-sdk-go/aws/session"
    "github.com/aws/aws-sdk-go/service/secretsmanager"
)

func getSecret(secretName string) (string, error) {
    sess := session.Must(session.NewSession())
    svc := secretsmanager.New(sess)

    input := &secretsmanager.GetSecretValueInput{
        SecretId: aws.String(secretName),
    }

    result, err := svc.GetSecretValue(input)
    if err != nil {
        return "", err
    }

    return *result.SecretString, nil
}

// Usage
dbPassword, _ := getSecret("prod/db/password")

HashiCorp Vault:

import "github.com/hashicorp/vault/api"

func getVaultSecret(path string) (string, error) {
    client, _ := api.NewClient(&api.Config{
        Address: "https://vault.example.com",
    })

    client.SetToken(os.Getenv("VAULT_TOKEN"))

    secret, err := client.Logical().Read(path)
    if err != nil {
        return "", err
    }

    return secret.Data["value"].(string), nil
}

// Usage
apiKey, _ := getVaultSecret("secret/data/api-key")

Secret Rotation

type SecretCache struct {
    mu      sync.RWMutex
    secret  string
    lastFetch time.Time
    ttl     time.Duration
}

func (c *SecretCache) Get() string {
    c.mu.RLock()
    if time.Since(c.lastFetch) < c.ttl {
        defer c.mu.RUnlock()
        return c.secret
    }
    c.mu.RUnlock()

    // Refresh secret
    c.mu.Lock()
    defer c.mu.Unlock()

    newSecret, _ := getSecret("my-secret")
    c.secret = newSecret
    c.lastFetch = time.Now()

    return c.secret
}

var apiKeyCache = &SecretCache{ttl: 5 * time.Minute}

// Usage
func callAPI() {
    apiKey := apiKeyCache.Get()
    // Use apiKey...
}

Dependency Security

Automated Scanning

Go:

# Check vulnerabilities in dependencies
go list -json -m all | nancy sleuth

# Or use govulncheck
govulncheck ./...

package.json (Node.js):

npm audit
npm audit fix

# Or use Snyk
snyk test

Dependabot (GitHub):

# .github/dependabot.yml
version: 2
updates:
  - package-ecosystem: "gomod"
    directory: "/"
    schedule:
      interval: "weekly"
    open-pull-requests-limit: 10

Pinning Dependencies

// go.mod
module myapp

go 1.21

require (
    github.com/gin-gonic/gin v1.9.1  // ✅ Pinned version
    // NOT: github.com/gin-gonic/gin v1.9  (minor updates allowed)
)

Docker:

# ❌ BAD: latest tag
FROM golang:latest

# ✅ GOOD: Specific version + SHA
FROM golang:1.21.1@sha256:abc123...

Supply Chain Security

Verify checksums:

# Go modules verify by default
go mod verify

# Download dependencies and verify
go mod download

Sign commits:

git config --global user.signingkey <GPG_KEY_ID>
git config --global commit.gpgSign true

Error Handling: Don't Leak Information

❌ Verbose Error Messages

// 🔥 BAD: Exposes DB structure & credentials
func getUser(id int) (*User, error) {
    var user User
    err := db.QueryRow("SELECT * FROM users WHERE id = ?", id).Scan(&user)
    if err != nil {
        return nil, fmt.Errorf("database error: %v", err)
        // Error: database error: pq: password authentication failed for user "admin"
    }
    return &user, nil
}

✅ Generic Error Messages (to users)

// ✅ GOOD: Generic message to user, detailed log internally
func getUser(id int) (*User, error) {
    var user User
    err := db.QueryRow("SELECT * FROM users WHERE id = ?", id).Scan(&user)
    if err != nil {
        log.Error("DB query failed", "error", err, "user_id", id)
        return nil, errors.New("failed to retrieve user")
        // User sees: "failed to retrieve user"
        // Log contains: full error details
    }
    return &user, nil
}

Structured Error Responses (API)

type ErrorResponse struct {
    Error   string `json:"error"`
    Code    string `json:"code"`
    Message string `json:"message"`
}

func handleError(w http.ResponseWriter, err error, code int) {
    log.Error("Request failed", "error", err)

    resp := ErrorResponse{
        Error:   "internal_server_error",
        Code:    "ERR_INTERNAL",
        Message: "An error occurred. Please try again later.",
    }

    w.Header().Set("Content-Type", "application/json")
    w.WriteHeader(code)
    json.NewEncoder(w).Encode(resp)
}

// Usage
func handleRequest(w http.ResponseWriter, r *http.Request) {
    user, err := getUser(123)
    if err != nil {
        handleError(w, err, 500)
        return
    }
    // ...
}

Rate Limiting & Throttling

Prevent brute-force attacks and DoS.

Token Bucket Algorithm

import "golang.org/x/time/rate"

var limiter = rate.NewLimiter(rate.Limit(10), 20)
// 10 requests per second, burst of 20

func rateLimitMiddleware(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        if !limiter.Allow() {
            http.Error(w, "Too many requests", 429)
            return
        }
        next.ServeHTTP(w, r)
    })
}

Per-user Rate Limiting

type RateLimiter struct {
    limiters map[string]*rate.Limiter
    mu       sync.Mutex
}

func NewRateLimiter() *RateLimiter {
    return &RateLimiter{
        limiters: make(map[string]*rate.Limiter),
    }
}

func (rl *RateLimiter) GetLimiter(key string) *rate.Limiter {
    rl.mu.Lock()
    defer rl.mu.Unlock()

    limiter, exists := rl.limiters[key]
    if !exists {
        limiter = rate.NewLimiter(rate.Limit(5), 10) // 5 req/sec, burst 10
        rl.limiters[key] = limiter
    }

    return limiter
}

var globalRateLimiter = NewRateLimiter()

func rateLimitByIP(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        ip := r.RemoteAddr
        limiter := globalRateLimiter.GetLimiter(ip)

        if !limiter.Allow() {
            http.Error(w, "Rate limit exceeded", 429)
            return
        }

        next.ServeHTTP(w, r)
    })
}

Distributed Rate Limiting (Redis)

import "github.com/redis/go-redis/v9"

func checkRateLimit(key string, limit int, window time.Duration) (bool, error) {
    ctx := context.Background()
    
    // Increment counter
    count, err := rdb.Incr(ctx, "ratelimit:"+key).Result()
    if err != nil {
        return false, err
    }

    // Set expiry on first request
    if count == 1 {
        rdb.Expire(ctx, "ratelimit:"+key, window)
    }

    return count <= int64(limit), nil
}

// Usage
func loginHandler(w http.ResponseWriter, r *http.Request) {
    email := r.FormValue("email")

    allowed, _ := checkRateLimit(email, 5, 15*time.Minute)
    if !allowed {
        http.Error(w, "Too many login attempts", 429)
        return
    }

    // Proceed with login...
}

Logging & Monitoring

What to Log

✅ Authentication attempts (success & failure)
✅ Authorization failures
✅ Input validation failures
✅ Critical errors
✅ Configuration changes
✅ Sensitive operations (delete account, change password)

❌ DON'T log: Passwords, tokens, credit cards, PII

Structured Logging

import "log/slog"

func handleLogin(w http.ResponseWriter, r *http.Request) {
    email := r.FormValue("email")
    password := r.FormValue("password")

    user, err := authenticateUser(email, password)
    if err != nil {
        slog.Warn("Login failed",
            "email", email,
            "ip", r.RemoteAddr,
            "user_agent", r.UserAgent(),
            "error", err,
        )
        http.Error(w, "Invalid credentials", 401)
        return
    }

    slog.Info("Login successful",
        "user_id", user.ID,
        "email", email,
        "ip", r.RemoteAddr,
    )

    // ...
}

Security Alerts

func detectAnomalousLogin(userID int64, ip string) {
    // Get user's typical login locations
    locations := getUserLoginLocations(userID)

    geoIP := getGeoIP(ip)
    if !contains(locations, geoIP.Country) {
        slog.Warn("Login from new location",
            "user_id", userID,
            "ip", ip,
            "country", geoIP.Country,
            "alert", true,
        )

        // Send alert email/SMS
        sendSecurityAlert(userID, "Login from "+geoIP.Country)
    }
}

Tóm tắt: Secure Coding Checklist

  • Input Validation: Whitelist, type-check, sanitize
  • Output Encoding: HTML escape, JSON encode, URL encode
  • Parameterized Queries: Never concatenate SQL
  • Security Headers: CSP, HSTS, X-Frame-Options
  • CSRF Protection: Tokens + SameSite cookies
  • Secret Management: Environment vars or Secrets Manager
  • Dependency Scanning: Automated vuln checks
  • Error Handling: Generic messages to users, detailed logs internally
  • Rate Limiting: Prevent brute-force
  • Logging: Log security events, not sensitive data

Bước tiếp theo

  • crypto-basics.md — Hashing, encryption fundamentals
  • api-and-web-security.md — API-specific security
  • interview-and-big-picture.md — Security interview questions