Let’s be honest: most security vulnerabilities aren’t clever zero-days; they’re us, the developers, leaving the front door wide open with a welcome mat that says “PLEASE INJECT HERE.” The good news? In Go, slamming that door shut is often straightforward, provided you know which doors exist. We’re going to tour the most common ones and arm you with the tools to deadbolt them.

SQL Injection: Your Query is Not a String Builder

If you take one thing from this section, let it be this: never, ever concatenate user input directly into a SQL query. I don’t care how much you sanitize it in your head. Don’t do it. This isn’t a questionable design choice; it’s a cardinal sin.

// BAD: This is how you get your database pwned.
query := "SELECT * FROM users WHERE email = '" + email + "';"
rows, err := db.Query(query)

See that? It’s a vulnerability waiting to happen. If email is ' OR '1'='1'--, that query becomes SELECT * FROM users WHERE email = '' OR '1'='1'--';, which dumps your entire users table. The fix is so simple it’s almost embarrassing: use parameterized queries. Every SQL driver worth its salt supports them. The database driver handles the escaping for you, separating the instruction (the query) from the data (the parameters).

// GOOD: Let the driver handle the dirty work.
query := "SELECT * FROM users WHERE email = ?;"
rows, err := db.Query(query, email) // <-- The email is passed as a parameter

// Even better, use a prepared statement for repeated queries.
stmt, err := db.Prepare("INSERT INTO products (name, price) VALUES (?, ?)")
if err != nil {
    log.Fatal(err)
}
defer stmt.Close()
_, err = stmt.Exec("Solid Gold Toilet", 1000000)

Why is this so robust? The database engine receives the query structure and the data separately. It knows the data is data, not part of the command. It’s the difference between handing a bartender a recipe (a parameter) and screaming ingredients at them while they’re trying to mix (concatenation).

Path Traversal: Escaping the Sandbox

Path traversal (or directory traversal) is when an attacker tricks your application into reading or writing files outside the directory you intended. Imagine you have a function that serves files based on a filename from the URL: yoursite.com/download?file=annual_report.pdf. The classic attack is to pass file=../../../../etc/passwd.

Your job is to normalize the path and then anchor it securely. Go provides the excellent filepath package for this, specifically the filepath.Join and filepath.Clean functions. These will resolve ../ and . sequences.

But here’s the critical, non-negotiable step: you must check that the final, cleaned path is under the directory you want to allow access to.

func safeReadFile(baseDir, userPath string) ([]byte, error) {
    // 1. Use filepath.Join to clean the path. This handles all the ../ nonsense.
    requestedPath := filepath.Join(baseDir, userPath)
    
    // 2. Clean it again to get the absolute, minimal representation.
    cleanedPath := filepath.Clean(requestedPath)
    
    // 3. THE MOST IMPORTANT STEP: Verify the result is still under your base directory!
    if !strings.HasPrefix(cleanedPath, filepath.Clean(baseDir)+string(os.PathSeparator)) {
        return nil, fmt.Errorf("nice try, hackerboy: path traversal attempt detected")
    }
    
    // 4. Now it's safe to use.
    return os.ReadFile(cleanedPath)
}

Why the explicit check? Because filepath.Clean alone isn’t enough. If your baseDir is /var/www, and the user asks for ../../etc/passwd, filepath.Join("/var/www", "../../etc/passwd") correctly returns /etc/passwd. It did its job perfectly. It’s now your job to say, “Wait a minute, /etc/passwd is not under /var/www!” The strings.HasPrefix check is that guard.

SSRF: Your Server’s Unintentional Proxy

Server-Side Request Forgery (SSRF) is a nasty one. It’s when an attacker forces your backend to make HTTP requests to internal systems you never meant to expose. Think of it as using your server as a jumping-off point to attack your own internal network. The classic scenario is a webhook tester or a PDF generator that fetches a URL provided by the user.

The naive implementation is simple and dangerous:

// BAD: An SSRF factory.
func handler(w http.ResponseWriter, r *http.Request) {
    url := r.FormValue("url")
    resp, err := http.Get(url) // <-- Yikes!
    // ... process response ...
}

An attacker can pass url=http://169.254.169.254/latest/meta-data/ to hit the AWS metadata service from your server, potentially grabbing IAM credentials. Or url=file:///etc/passwd. Or url=http://internal-payroll-database/employees/salaries.

Fixing this requires a multi-layered approach:

  1. Validate the input URL aggressively. Use a denylist? Forget it. The number of internal IP ranges and sneaky DNS tricks is endless. Use an allowlist of permitted domains and schemes instead.

    func isValidURL(inputUrl string) bool {
        u, err := url.Parse(inputUrl)
        if err != nil {
            return false
        }
        // Only allow HTTPS to known, good domains.
        permittedHosts := map[string]bool{
            "api.github.com": true,
            "hooks.slack.com": true,
        }
        return u.Scheme == "https" && permittedHosts[u.Host]
    }
    
  2. Use a context-aware HTTP client with timeouts. Go’s default client has no request timeout, which is a fantastic way to get sucked into a slowloris attack.

    client := &http.Client{
        Timeout: 10 * time.Second, // Seriously, always set a timeout.
    }
    
  3. Disable Redirects. Redirects can be used to bypass your host allowlist. The attacker gives you https://good.com, which redirects to http://bad-internal-server. You need to check the redirects yourself.

    client := &http.Client{
        Timeout: 10 * time.Second,
        CheckRedirect: func(req *http.Request, via []*http.Request) error {
            // You could add logic here to check the redirect target
            // against your allowlist before following it.
            return http.ErrUseLastResponse // Or just don't follow redirects at all.
        },
    }
    

SSRF is tricky because the “right” solution depends heavily on your threat model. The gold standard is to have a tightly defined allowlist and to never, ever let the client control the scheme or the host. It’s a pain, but it’s less of a pain than explaining a breach to your CTO.