How to Write Secure Go Code
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?
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:
Define Input Schemas: Use strict schemas to define valid input. Libraries like
go-playground/validator
can help enforce these rules.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.
Use Parameterized Queries: Prevent SQL injection using parameterized queries rather than string concatenation.
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:
Use native libraries: Instead of relying on external command-line programs, use Go's native libraries.
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:
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.
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 nothttp
or others.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.
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:
Play Fetch the Flag
Test your security skills in our CTF event on February 27, from 9 am - 9 pm ET.