Security of Go Applications: Top 10 Vulnerabilities in Practice
Go (Golang) is a language renowned for its performance and built-in security mechanisms. However, like any tool, it does not fully protect the developer from mistakes. Many vulnerabilities arise not from the language itself, but from the incorrect use of its features: working with goroutines, channels, pointers, or third-party libraries.
In this article, we will break down the 10 most common vulnerabilities in Go applications. Each point contains a real code example, an explanation of the problem, and a practical tip for fixing it. The material will be useful for both beginner Go developers and experienced engineers looking to refresh their knowledge.
1. SQL Injections via String Concatenation
The most classic mistake is forming an SQL query by concatenating strings with user data. Go provides excellent drivers (e.g., database/sql), but does not protect against silly mistakes.
Vulnerable code:
func getUser(db *sql.DB, username string) (*User, error) { query := "SELECT id, name FROM users WHERE username = '" + username + "'" rows, err := db.Query(query) // ...}An attacker can pass admin' OR '1'='1 and retrieve all users.
Solution: Always use parameterized queries.
func getUserSafe(db *sql.DB, username string) (*User, error) { query := "SELECT id, name FROM users WHERE username = $1" row := db.QueryRow(query, username) // ...}2. Race Condition (Data Race) in Goroutines
Go is a language with powerful concurrency support, but without synchronization, access to shared data from different goroutines leads to undefined behavior.
Vulnerable code:
var counter int
func increment() { for i := 0; i < 1000; i++ { counter++ // not atomic }}
func main() { go increment() go increment() time.Sleep(time.Second) fmt.Println(counter) // result is unpredictable}Solution: Use mutexes (sync.Mutex) or atomic operations (sync/atomic).
var mu sync.Mutexvar counter int
func incrementSafe() { for i := 0; i < 1000; i++ { mu.Lock() counter++ mu.Unlock() }}3. Goroutine Leak
If a launched goroutine blocks forever (e.g., waiting for data in a channel that will never close), it "leaks", consuming memory.
Vulnerable code:
func process(jobs <-chan int) { for j := range jobs { go func(job int) { time.Sleep(time.Hour) // long-running task fmt.Println(job) }(j) }}If the jobs channel is not closed, the goroutines will hang forever.
Solution: Use context (context.Context) for cancellation and always control the lifetime of goroutines.
func processSafe(ctx context.Context, jobs <-chan int) { for j := range jobs { select { case <-ctx.Done(): return default: go func(job int) { select { case <-time.After(time.Hour): fmt.Println(job) case <-ctx.Done(): return } }(j) } }}4. Incorrect Error Handling (panic/recover)
Using panic() for handling ordinary errors is bad practice. If a panic is not caught, the application will crash. However, overusing recover() can also hide critical issues.
Vulnerable code: