Go has become a popular choice for building APIs due to its performance, simplicity, and excellent concurrency model. However, Go's minimalist approach means developers must be deliberate about security—the language provides the tools, but won't enforce their use. This guide covers essential security patterns for Go APIs, from input validation to SQL injection prevention, and explains how agentic penetration testing can help identify vulnerabilities before attackers do.
Understanding Go API Security Risks
Go APIs face many of the same threats as applications written in other languages: injection attacks, broken authentication, sensitive data exposure, and more. However, Go's characteristics create unique considerations. The standard library provides low-level primitives rather than high-level abstractions, meaning developers often implement security controls from scratch. Without frameworks enforcing patterns, inconsistent security implementations can emerge across different parts of an application.
Common vulnerability patterns in Go APIs include:
- SQL injection through string concatenation in database queries
- Command injection via unsafe use of
os/exec - Path traversal when handling file operations
- Insecure deserialization of JSON or other formats
- Race conditions in concurrent code affecting security state
- Weak cryptographic implementations using inappropriate algorithms
The impact of these vulnerabilities can be severe: data breaches, unauthorized access, service disruption, and compliance violations. Understanding Go-specific security patterns is essential for building resilient APIs.
Input Validation in Go
Effective input validation is the first line of defense against most injection attacks. Go's strong typing helps, but isn't sufficient on its own—you must validate the semantic correctness of inputs, not just their types.
Validation with Struct Tags
Using validation libraries with struct tags provides declarative, maintainable validation:
package main
import (
"net/http"
"github.com/go-playground/validator/v10"
)
type CreateUserRequest struct {
Email string `json:"email" validate:"required,email,max=255"`
Username string `json:"username" validate:"required,alphanum,min=3,max=32"`
Password string `json:"password" validate:"required,min=12,max=128"`
Age int `json:"age" validate:"omitempty,gte=13,lte=120"`
}
var validate = validator.New()
func CreateUserHandler(w http.ResponseWriter, r *http.Request) {
var req CreateUserRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
http.Error(w, "Invalid JSON", http.StatusBadRequest)
return
}
if err := validate.Struct(req); err != nil {
validationErrors := err.(validator.ValidationErrors)
http.Error(w, formatValidationErrors(validationErrors), http.StatusBadRequest)
return
}
// Process valid input
}Custom Validators
For domain-specific rules, create custom validators:
func init() {
validate.RegisterValidation("safepath", func(fl validator.FieldLevel) bool {
path := fl.Field().String()
// Reject path traversal attempts
if strings.Contains(path, "..") || strings.HasPrefix(path, "/") {
return false
}
// Only allow alphanumeric, dash, underscore, and single dots
matched, _ := regexp.MatchString(`^[a-zA-Z0-9_\-\.]+$`, path)
return matched
})
}
type FileRequest struct {
Filename string `json:"filename" validate:"required,safepath,max=255"`
}Sanitizing String Inputs
Beyond validation, sanitize inputs that will be used in sensitive contexts:
import "html"
func sanitizeForHTML(input string) string {
return html.EscapeString(input)
}
func sanitizeForLogging(input string) string {
// Remove newlines to prevent log injection
sanitized := strings.ReplaceAll(input, "\n", "")
sanitized = strings.ReplaceAll(sanitized, "\r", "")
// Limit length
if len(sanitized) > 1000 {
sanitized = sanitized[:1000]
}
return sanitized
}SQL Injection Prevention with database/sql
SQL injection remains one of the most dangerous vulnerabilities. Go's database/sql package provides parameterized queries, but developers must use them correctly.
The Wrong Way: String Concatenation
Never construct SQL queries through string concatenation:
// VULNERABLE - Never do this
func getUser(db *sql.DB, username string) (*User, error) {
query := "SELECT id, email FROM users WHERE username = '" + username + "'"
row := db.QueryRow(query)
// An attacker could inject: ' OR '1'='1
}The Right Way: Parameterized Queries
Always use parameterized queries with placeholders:
func getUser(db *sql.DB, username string) (*User, error) {
query := "SELECT id, email FROM users WHERE username = $1"
row := db.QueryRow(query, username)
var user User
err := row.Scan(&user.ID, &user.Email)
if err != nil {
return nil, err
}
return &user, nil
}
func searchUsers(db *sql.DB, searchTerm string, limit int) ([]User, error) {
query := `
SELECT id, username, email
FROM users
WHERE username ILIKE $1 OR email ILIKE $1
LIMIT $2
`
// Wrap search term for LIKE query
searchPattern := "%" + searchTerm + "%"
rows, err := db.Query(query, searchPattern, limit)
if err != nil {
return nil, err
}
defer rows.Close()
var users []User
for rows.Next() {
var u User
if err := rows.Scan(&u.ID, &u.Username, &u.Email); err != nil {
return nil, err
}
users = append(users, u)
}
return users, rows.Err()
}Dynamic Query Building
When you need dynamic queries (varying WHERE clauses, sorting), use a query builder:
import "github.com/Masterminds/squirrel"
func searchUsersAdvanced(db *sql.DB, filters UserFilters) ([]User, error) {
psql := squirrel.StatementBuilder.PlaceholderFormat(squirrel.Dollar)
query := psql.Select("id", "username", "email", "created_at").
From("users")
if filters.Username != "" {
query = query.Where(squirrel.ILike{"username": "%" + filters.Username + "%"})
}
if filters.Email != "" {
query = query.Where(squirrel.Eq{"email": filters.Email})
}
if filters.CreatedAfter != nil {
query = query.Where(squirrel.GtOrEq{"created_at": filters.CreatedAfter})
}
// Whitelist allowed sort columns
allowedSortColumns := map[string]bool{
"username": true, "created_at": true, "email": true,
}
if allowedSortColumns[filters.SortBy] {
query = query.OrderBy(filters.SortBy + " " + sanitizeSortOrder(filters.SortOrder))
}
sql, args, err := query.ToSql()
if err != nil {
return nil, err
}
return executeUserQuery(db, sql, args)
}
func sanitizeSortOrder(order string) string {
if strings.ToUpper(order) == "DESC" {
return "DESC"
}
return "ASC"
}Authentication and Authorization Patterns
Secure authentication and authorization are critical for Go APIs. Here are patterns that help prevent common vulnerabilities.
JWT Validation
When using JWTs, validate thoroughly:
import "github.com/golang-jwt/jwt/v5"
type Claims struct {
UserID string `json:"user_id"`
Roles []string `json:"roles"`
jwt.RegisteredClaims
}
func validateToken(tokenString string, secret []byte) (*Claims, error) {
token, err := jwt.ParseWithClaims(tokenString, &Claims{}, func(token *jwt.Token) (interface{}, error) {
// Verify signing method to prevent algorithm confusion attacks
if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"])
}
return secret, nil
})
if err != nil {
return nil, err
}
claims, ok := token.Claims.(*Claims)
if !ok || !token.Valid {
return nil, errors.New("invalid token")
}
return claims, nil
}Middleware for Authorization
Implement authorization checks as middleware:
func RequireAuth(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
authHeader := r.Header.Get("Authorization")
if authHeader == "" {
http.Error(w, "Missing authorization header", http.StatusUnauthorized)
return
}
tokenString := strings.TrimPrefix(authHeader, "Bearer ")
claims, err := validateToken(tokenString, jwtSecret)
if err != nil {
http.Error(w, "Invalid token", http.StatusUnauthorized)
return
}
ctx := context.WithValue(r.Context(), "claims", claims)
next.ServeHTTP(w, r.WithContext(ctx))
})
}
func RequireRole(role string) func(http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
claims := r.Context().Value("claims").(*Claims)
hasRole := false
for _, r := range claims.Roles {
if r == role {
hasRole = true
break
}
}
if !hasRole {
http.Error(w, "Forbidden", http.StatusForbidden)
return
}
next.ServeHTTP(w, r)
})
}
}Secure Error Handling
Error handling in Go requires care to avoid leaking sensitive information:
type APIError struct {
Code string `json:"code"`
Message string `json:"message"`
}
func handleError(w http.ResponseWriter, err error, statusCode int) {
// Log the full error internally
log.Printf("Error: %v", err)
// Return a safe error to the client
apiErr := APIError{
Code: http.StatusText(statusCode),
Message: getSafeErrorMessage(err, statusCode),
}
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(statusCode)
json.NewEncoder(w).Encode(apiErr)
}
func getSafeErrorMessage(err error, statusCode int) string {
switch statusCode {
case http.StatusBadRequest:
return "Invalid request format"
case http.StatusUnauthorized:
return "Authentication required"
case http.StatusForbidden:
return "Access denied"
case http.StatusNotFound:
return "Resource not found"
default:
return "An error occurred"
}
}Why Traditional Pentesting Falls Short
Manual penetration testing for Go APIs faces several challenges. Testers must understand Go-specific patterns and idioms to identify vulnerabilities effectively. They need to trace data flow through concurrent code, understand context propagation, and recognize when the standard library's features are being misused. Traditional pentests are also point-in-time assessments—by the time results arrive, the codebase may have changed significantly, especially in fast-moving Go projects.
Additionally, Go's microservices architecture means security testing must cover multiple services, their interactions, and the boundaries between them. Manual testers struggle to systematically cover all endpoints, especially when APIs evolve rapidly.
How Agentic Testing Identifies Go Vulnerabilities
RedVeil's AI-powered penetration testing addresses these limitations through autonomous, intelligent security assessment. RedVeil's agents analyze your Go API endpoints, understand authentication flows, and systematically probe for vulnerabilities like SQL injection, broken access control, and input validation gaps.
Unlike static analysis tools that may flag theoretical issues, RedVeil validates vulnerabilities by attempting exploitation. When it finds a SQL injection point, it demonstrates the impact with actual query manipulation. When it discovers broken authorization, it shows exactly how an attacker could access unauthorized resources.
RedVeil runs on-demand, fitting into your development workflow. After deploying changes to your Go API, you can launch a security assessment and receive validated findings within hours, not weeks. Each finding includes reproduction steps, impact analysis, and remediation guidance specific to Go patterns.
Conclusion
Securing Go APIs requires understanding the language's characteristics and applying consistent security patterns. From input validation with struct tags to parameterized SQL queries and proper authentication middleware, Go provides the tools—but developers must use them correctly.
Traditional security testing struggles to keep pace with Go API development. On-demand, agentic testing from RedVeil fills this gap by providing autonomous, intelligent security assessment that validates real vulnerabilities and provides actionable remediation guidance.
Start securing your Go APIs with RedVeil today at https://app.redveil.ai/ and ensure your applications are protected against real-world attacks.