The file upload that broke production
You deploy a Go web app that handles file uploads. The tests pass. The load balancer is happy. Then a user uploads a zip archive containing a file named ../../../etc/passwd. Your extraction code opens that path, and suddenly your server is reading system files it shouldn't touch. Go protects you from buffer overflows and race conditions, but it won't stop you from opening a malicious file path if you ask it to. Security in Go web apps requires explicit constraints. You have to tell the runtime what is allowed, not just what is possible.
Go's type system catches mistakes at compile time, but security vulnerabilities often live in logic gaps. A path traversal isn't a type error. It's a logic error where the application trusts user input too much. The standard library provides tools to mitigate these risks, but you must enable them and use them correctly. Defense in depth means validating input, enforcing transport security, managing secrets properly, and configuring the runtime to reject dangerous patterns.
Archive path traversal
The archive/tar and archive/zip packages used to allow entries with paths like ../ to escape the extraction directory. This is the classic path traversal attack. Modern Go versions mitigate this, but the mitigation can be disabled or bypassed in older code. You should enforce strict path validation.
Here's how to safely extract a zip file by validating every entry path against the target directory.
// ExtractZip reads a zip archive and writes files to destDir.
// It rejects any entry that attempts to escape the destination directory.
func ExtractZip(r *zip.ReadCloser, destDir string) error {
// Iterate over entries in the archive.
for _, f := range r.File {
// Clean the path to resolve .. and . components.
cleanPath := filepath.Clean(f.Name)
// Join with destination and clean again to check for traversal.
targetPath := filepath.Join(destDir, cleanPath)
// Ensure the resolved path is still inside destDir.
// If not, the archive contains a malicious path.
if !strings.HasPrefix(targetPath, filepath.Clean(destDir)+string(os.PathSeparator)) {
return fmt.Errorf("illegal file path: %s", f.Name)
}
// Open the file within the archive for reading.
rc, err := f.Open()
if err != nil {
return fmt.Errorf("failed to open archive entry: %w", err)
}
// Create the target file on disk.
outFile, err := os.Create(targetPath)
if err != nil {
rc.Close()
return fmt.Errorf("failed to create output file: %w", err)
}
// Copy data from archive to disk.
_, err = io.Copy(outFile, rc)
outFile.Close()
rc.Close()
if err != nil {
return fmt.Errorf("failed to write file: %w", err)
}
}
return nil
}
The filepath.Clean function removes redundant elements like .. and .. Joining the cleaned path with the destination and checking the prefix ensures the final path stays within bounds. If the prefix check fails, the archive is malicious.
Trust no path. Validate every component before opening a file.
Runtime safety with GODEBUG
The Go runtime provides a debug environment variable to enforce path safety in archives. Setting GODEBUG=tarinsecurepath=0,zipinsecurepath=0 makes the standard library reject insecure paths automatically. This is a safety net.
Enforce archive safety at the runtime level by setting the GODEBUG variable before starting your application.
# Enable strict path validation for tar and zip archives.
# This prevents the standard library from processing entries with traversal sequences.
export GODEBUG=tarinsecurepath=0,zipinsecurepath=0
# Run the application with the safety net active.
go run main.go
This setting affects the archive/tar and archive/zip packages globally. It causes them to return errors when they encounter paths that escape the root. Use this in production to add a layer of protection even if your code has a validation bug.
GODEBUG is a safety net. Use it, but don't rely on it as your only check.
Input validation at the edge
Web apps receive data from the network. The network is hostile. Every query parameter, header, and body field must be validated. Go doesn't validate input by default. r.FormValue("id") returns a string. If you expect an integer, you must parse and check it.
If you try to use a string where an int is expected, the compiler rejects this with cannot use string as int. This helps, but runtime parsing can fail. You must handle parse errors and range checks.
Define a struct for your input and validate fields explicitly before processing the request.
// CreateUserRequest holds validated data for creating a user.
type CreateUserRequest struct {
// Email must be a valid email format.
Email string `json:"email"`
// Age must be between 0 and 120.
Age int `json:"age"`
}
// Validate checks the request fields and returns an error if invalid.
func (r *CreateUserRequest) Validate() error {
// Check email format using a regex or validation library.
if !isValidEmail(r.Email) {
return fmt.Errorf("invalid email format")
}
// Check age range to prevent negative values or unrealistic numbers.
if r.Age < 0 || r.Age > 120 {
return fmt.Errorf("age out of range")
}
return nil
}
The receiver name r matches the type CreateUserRequest. This is the Go convention for receiver naming. Use one or two letters. The struct fields are public so encoding/json can decode them. Validation happens in a method, keeping the logic encapsulated.
Validate at the boundary. Trust nothing from the client.
Template safety and XSS
Cross-site scripting attacks occur when user input is rendered in HTML without escaping. Go's html/template package auto-escapes content based on context. This prevents most XSS vulnerabilities.
Use html/template for web responses. Never use text/template for HTML output. text/template does not escape HTML entities.
// RenderUserPage executes a template with user data and writes to the response.
func RenderUserPage(w http.ResponseWriter, r *http.Request) {
// Parse the template from the file system.
// html/template auto-escapes variables based on context.
tmpl, err := template.ParseFiles("user.html")
if err != nil {
http.Error(w, "internal error", http.StatusInternalServerError)
return
}
// Execute the template with the user data.
// Variables like {{.Name}} are escaped automatically.
err = tmpl.Execute(w, map[string]string{
"Name": r.FormValue("name"),
})
if err != nil {
http.Error(w, "internal error", http.StatusInternalServerError)
return
}
}
The html/template package analyzes the template syntax and applies the correct escaping for HTML, JavaScript, CSS, and URL contexts. If you pass a string containing <script>alert(1)</script> to a variable, the template renders it as safe text.
Auto-escaping saves you from XSS. Use html/template for every web response.
Secrets and cryptographic randomness
Generating tokens, session IDs, or passwords requires cryptographic randomness. math/rand is deterministic and predictable. An attacker can guess the next value if they know the seed. Use crypto/rand instead.
Generate a secure random token using crypto/rand to prevent prediction attacks.
// GenerateToken creates a cryptographically secure random token.
func GenerateToken(length int) (string, error) {
// Create a byte slice for the random data.
b := make([]byte, length)
// Read random bytes from the OS entropy source.
// This blocks until sufficient entropy is available.
_, err := rand.Read(b)
if err != nil {
return "", fmt.Errorf("failed to generate random bytes: %w", err)
}
// Encode to base64 for safe transmission in URLs or headers.
return base64.URLEncoding.EncodeToString(b), nil
}
The crypto/rand.Read function returns an error. Handle it. If the OS cannot provide entropy, the function fails. This is rare but possible. The underscore _ discards the number of bytes read because Read fills the slice or returns an error. You don't need the count if you check the error.
Predictable randomness is a backdoor. Use crypto/rand for anything security-related.
HTTP headers and transport security
Browsers enforce security policies based on headers. Missing headers can lead to XSS, clickjacking, or MIME sniffing attacks. Set headers like Strict-Transport-Security, Content-Security-Policy, and X-Content-Type-Options.
Use middleware to inject security headers into every response.
// SecurityHeaders adds standard security headers to the response.
func SecurityHeaders(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// Enforce HTTPS to prevent downgrade attacks.
w.Header().Set("Strict-Transport-Security", "max-age=31536000; includeSubDomains")
// Prevent the page from being embedded in frames.
w.Header().Set("X-Frame-Options", "DENY")
// Disable MIME type sniffing.
w.Header().Set("X-Content-Type-Options", "nosniff")
// Delegate to the next handler in the chain.
next.ServeHTTP(w, r)
})
}
Middleware accepts an http.Handler interface and returns a new http.Handler. This follows the "accept interfaces, return structs" mantra. The middleware wraps the handler and adds headers before calling the next step.
Headers are the browser's security policy. Set them early and consistently.
TLS configuration
HTTPS is mandatory for web applications. Go's net/http server supports TLS, but you should configure the tls.Config explicitly. The default configuration is reasonable, but setting MinVersion ensures you don't accept outdated protocols.
Configure the TLS server to reject weak protocols and enforce modern ciphers.
// NewTLSServer creates an HTTP server with strict TLS settings.
func NewTLSServer(handler http.Handler) *http.Server {
return &http.Server{
Addr: ":443",
Handler: handler,
TLSConfig: &tls.Config{
// Require TLS 1.2 or higher.
MinVersion: tls.VersionTLS12,
// Prefer server cipher suites.
PreferServerCipherSuites: true,
},
}
}
The MinVersion field sets the minimum TLS version. TLS 1.0 and 1.1 have known vulnerabilities. Require TLS 1.2 or 1.3. The PreferServerCipherSuites option ensures the server chooses the cipher suite, not the client.
TLS config matters. Set MinVersion to reject weak protocols.
Dependencies and supply chain
Go modules manage dependencies. Vulnerabilities can exist in third-party code. Use govulncheck to scan for known vulnerabilities.
Scan your codebase for known vulnerabilities in dependencies using govulncheck.
# Install govulncheck tool.
go install golang.org/x/vuln/cmd/govulncheck@latest
# Scan the current module for vulnerabilities.
govulncheck ./...
The govulncheck tool analyzes your code and dependencies against a database of known vulnerabilities. It reports issues with links to advisories. Run this in your CI/CD pipeline to catch problems before deployment.
Your code is only as safe as your dependencies. Scan regularly.
Error handling and information leakage
Don't leak stack traces to clients. Log errors internally. Return generic messages externally. If a database query fails, the error might contain table names or query structure. An attacker can use this information to craft further attacks.
If you forget to import a package, you get undefined: pkg from the compiler. If you forget to use one, you get imported and not used. These are compile-time checks. Runtime errors need care. Wrap errors with fmt.Errorf and %w to preserve context without leaking details.
Stack traces are a roadmap for attackers. Log internally, hide externally.
Reporting vulnerabilities
If you find a vulnerability in Go itself, report it to security@golang.org. Check the SECURITY.md file in the Go repository for details. The Go team has a process for handling security reports.
Find a bug? Report it. Don't tweet it.
Decision matrix
Use crypto/rand when generating tokens, session IDs, or passwords. Use math/rand when simulating data or generating non-security identifiers. Use GODEBUG=tarinsecurepath=0,zipinsecurepath=0 when processing untrusted archives to enforce runtime path validation. Use manual path validation when you need fine-grained control over allowed file names or directory structures. Use govulncheck when running CI/CD pipelines or before production deployments. Use middleware for headers when you need consistent security policies across all routes. Use html/template when rendering HTML to prevent XSS. Use text/template only for non-HTML output like emails or logs. Use tls.Config.MinVersion when configuring HTTPS to reject weak protocols.