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: <script>alert('XSS')</script>
}
// 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)
})
}
SameSite Cookie Attribute
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