Security of Go Applications: Top 10 Vulnerabilities in Practice

Online Python Trainer for Beginners

Learn Python easily without overwhelming theory. Solve practical tasks with automatic checking, get hints in Russian, and write code directly in your browser — no installation required.

Start Course

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:

                

Blogs

Book Recommendations