Skip to main content

How to Write Secure Go Code

Written by:
0 mins read

Let’s discuss the importance of writing secure code in Go. Writing secure code is a fundamental responsibility for developers, especially in a language like Go, which is widely used for building scalable and efficient systems. Go's simplicity and performance make it a popular choice for cloud-native applications, especially for its affiliation with Kubernetes ecosystem tooling, but security vulnerabilities can undermine these benefits. Ensuring your Go code is secure protects your application, safeguards user data, and maintains trust.

What of these common security vulnerabilities impact applications written in Go?

  1. Validating and sanitizing user input in Go

  2. Avoiding subshelling in Go

  3. Composing URLs securely

Overview of common security vulnerabilities in Go applications

Go applications are susceptible to various security vulnerabilities that can compromise your application's integrity, confidentiality, and availability (also known as the CIA triad in security jargon). Some of the most common vulnerabilities include:

  • SQL Injection occurs when untrusted input is concatenated into SQL queries, allowing attackers to manipulate the query execution.

  • Command Injection occurs when user input is improperly handled in shell commands, leading to arbitrary command execution.

  • Server-Side Request Forgery (SSRF) occurs when an application fetches a URL provided by the user, potentially allowing attackers to make requests to internal services.

The role of tools like Snyk Code in identifying and fixing vulnerabilities


Tools like Snyk Code are crucial in identifying and fixing vulnerabilities in Go applications. Snyk Code scans your source code for security issues, providing actionable insights and remediation advice. Integrating Snyk into your development workflow allows you to catch vulnerabilities early, reduce the risk of security breaches, and maintain a secure codebase.

A Go Example: Detecting SQL Injection with Snyk Code

Consider the following Go code snippet that is vulnerable to SQL injection:

package main

import (
	"database/sql"
	"fmt"
	"log"
	"net/http"

	_ "github.com/lib/pq"
)

func main() {
	http.HandleFunc("/user", func(w http.ResponseWriter, r *http.Request) {
		username := r.URL.Query().Get("username")
		db, err := sql.Open("postgres", "user=youruser dbname=yourdb sslmode=disable")
		if err != nil {
			log.Fatal(err)
		}
		defer db.Close()

		query := fmt.Sprintf("SELECT * FROM users WHERE username = '%s'", username)
		rows, err := db.Query(query)
		if err != nil {
			http.Error(w, "Database error", http.StatusInternalServerError)
			return
		}
		defer rows.Close()

		// Process rows...
	})

	log.Fatal(http.ListenAndServe(":8080", nil))
}

In this example, the username parameter is directly inserted into the SQL query, making it vulnerable to SQL injection. Snyk Code can detect this vulnerability and suggest using parameterized queries to prevent it:

query := "SELECT * FROM users WHERE username = $1"
rows, err := db.Query(query, username)

1. Validating and sanitizing user input in Go

Input validation and sanitization are crucial in safeguarding your Go applications against common attack vectors such as SQL Injection and Cross-Site Scripting (XSS). User input should never be trusted by query parameters, JSON response bodies from HTTP requests, or database contents. Always validate input against expected schemas, apply sanitization logic where necessary, and perform output encoding relevant to the context.

Best practices for input validation in Go

When dealing with user input in Go, adhere to these best practices:

  1. Define Input Schemas: Use strict schemas to define valid input. Libraries like go-playground/validator can help enforce these rules.

  2. Sanitize Input: Remove or escape potentially harmful characters from user input. For example, HTML-encode input to prevent XSS. This is largely a practice that is helpful and implemented in template engine tools.

  3. Use Parameterized Queries: Prevent SQL injection using parameterized queries rather than string concatenation.

  4. Output Encoding: Encode output based on the context, such as HTML encoding for web pages or JSON encoding for API responses.

A Go Example: Parameter binding for SQL injection

An SQL injection is a severe application-level security vulnerability that occurs when user input is directly included in SQL queries. To prevent this, use parameterized queries:

import (
    "database/sql"
    _ "github.com/go-sql-driver/mysql"
)

func getUser(db *sql.DB, userID string) (User, error) {
    var user User
    err := db.QueryRow("SELECT id, name FROM users WHERE id = ?", userID).Scan(&user.ID, &user.Name)
    if err != nil {
        return user, err
    }
    return user, nil
}

In this example, the ? placeholder in the SQL query is replaced by the userID parameter safely, preventing SQL injection attacks. This security best practice helps the database engine differentiate the query's original meaning from the values it uses.

2. Avoiding subshelling in Go

Subshelling in Go refers to launching subprocesses using functions like exec.CommandContext() or exec.Command(). While these functions offer flexibility, they also introduce significant security risks, particularly command injection vulnerabilities. Command injection occurs when an attacker manipulates input data to execute arbitrary commands on the host system. This can lead to unauthorized data access, data corruption, or even more significant system compromise risks.

Alternatives to subshelling in Go: Native library APIs, FFI, etc.

To mitigate the risks associated with subshelling, developers should prefer using Go's native library APIs or Foreign Function Interface (FFI) to perform tasks that might otherwise require external commands. Go's standard library is robust and provides many utilities for file manipulation, image processing, and more, which can often replace the need for subshelling.

A Go Example: Command injection in image manipulation

Consider the example where user input is used to manipulate an image file. This example demonstrates a common pitfall that can lead to command injection:

package main

import (
    "os/exec"
)

func manipulateImage(targetFilename string) error {
    convertCmd := fmt.Sprintf("convert %s -resize %sx%s %s", targetFilename, fileSize, fileSize, targetFilename)

    _, err = exec.CommandContext(ctx, "sh", "-c", convertCmd).CombinedOutput()
}

In this example, if targetFilename is derived from user input, an attacker could inject additional commands by crafting a malicious filename. For instance, a filename like image.png; rm -rf /tmp could execute a destructive command if not properly sanitized.

The risk here stems from the direct use of user input in constructing shell commands and also from the fact that the code doesn’t use prepared command arguments to build the command. To mitigate this risk, consider the following strategies:

  1. Use native libraries: Instead of relying on external command-line programs, use Go's native libraries. 

  2. Validate and sanitize input: Always validate and sanitize user input to ensure it conforms to expected patterns. This can prevent malicious input from being executed. The vulnerable code example should avoid a direct shell interpolation via sh -c instead run the command directly. However, that also has implications of argument injection vulnerabilities which need to be properly sanitized and escaped.

3. Composing URLs securely

The requirement for composing secure URLs directly relates to one of OWASP’s top 10 security vulnerabilities: Server-Side Request Forgery (SSRF). SSRF is a critical security vulnerability where an attacker can manipulate a server to send unintended HTTP requests, such as to internal resources, other private microservices, or generally any other unauthorized service call. This can lead to unauthorized access to internal services, data exfiltration, or even remote code execution. To understand SSRF in detail, I’ll refer to the SSRF Snyk Learn lesson.

Safe URL composition strategies in Go

When composing URLs in Go, especially when user input is involved, it is crucial to implement strategies that safeguard against SSRF. Here are some best practices:

  1. Validate URLs: Always validate user-provided URLs against an allow-list of vetted and approved domains or patterns. This ensures that only legitimate requests are made.

  2. Use URL parsing: Utilize Go's net/url package to parse and validate URLs. This helps break down the URL into its components and verify each part. For example, in this stage, you can apply a security control that ensures that only a specific set of supported protocol schemes (https://) are allowed and not http or others.

  3. Restrict internal network access: Ensure your application does not request internal IP addresses or sensitive endpoints by applying Kubernetes side-cars, HTTP proxies, API Gateways and other network-aware security controls.

  4. Timeouts and limits: While not directly related to SSRF, setting appropriate timeouts and response size limits for outgoing requests helps prevent resource exhaustion and large data exfiltration with the aim of minimizing an SSRF impact.

A Go Example: SSRF vulnerability from user-provided URLs

The following example demonstrates an unsafe way of handling user-provided URLs, leading to a potential SSRF vulnerability:

package main

import (
    "net/http"
)

func fetchUserProfileImage(userURL string) (*http.Response, error) {
    // Directly using user input to fetch remote data
    resp, err := http.Get(userURL)
    return resp, err
}

In this code, the fetchUserProfileImage function takes a URL from the user and directly uses it in an HTTP GET request. An attacker can exploit this to make requests to internal services.

Conclusion

Writing secure Go code requires vigilance and adherence to best practices. Here are the key security controls we discussed specifically for secure Go development:

  • Validation and sanitization of user input flowing through code paths in Go.

  • Avoid subshell operations and prefer native Go APIs and FFIs where they exist.

  • Treat URLs coming from user input with utmost prejudice to prevent SSRF vulnerabilities.

Utilize Snyk Code for continuous security scanning

Take the next step in securing your Go applications. Sign up for a free Snyk account today and start integrating security into your development workflow. With Snyk, you can confidently build secure applications and protect your users.

To ensure your Go code remains secure, leverage Snyk Code. It provides continuous security scanning, helping you identify and fix vulnerabilities in your codebase before they become a problem.

See Snyk Code in action in my VS Code IDE, where it detects and employs a machine learning engine to assist me with proposals on how to write better secure code in Golang:

Snyk Code finds security vulnerabilities in my Golang project

Play Fetch the Flag

Test your security skills in our CTF event on February 27, from 9 am - 9 pm ET.