Command Injection in Gin
How Command Injection Manifests in Gin
Command injection vulnerabilities in Gin applications typically occur when user input is directly passed to system commands without proper sanitization. In Gin, this often happens through HTTP parameters that control file operations, external process execution, or system interactions.
A common pattern is using os/exec with user-controlled input:
func backupHandler(c *gin.Context) {
filename := c.Query("file")
cmd := exec.Command("tar", "czf", "/backups/"+filename, "/data")
cmd.Run()
}
An attacker could exploit this by requesting /backup?file=backup.tar; rm -rf /, causing the shell to execute both commands.
Another Gin-specific pattern involves using os/exec with CombinedOutput() for logging or debugging:
func runScript(c *gin.Context) {
script := c.Query("script")
output, _ := exec.Command("sh", "-c", script).CombinedOutput()
c.JSON(200, gin.H{"output": string(output)})
}
This is particularly dangerous because it gives attackers direct shell access through the API.
Even seemingly safe operations can be vulnerable. Consider file path construction:
func readFile(c *gin.Context) {
path := c.Query("path")
content, _ := os.ReadFile("/var/www" + path)
c.String(200, string(content))
}
An attacker could use ../ sequences to read arbitrary files outside the intended directory.
Gin's middleware system can also introduce command injection if not careful. For example, a logging middleware that executes commands based on request data:
func loggingMiddleware() gin.HandlerFunc {
return func(c *gin.Context) {
userAgent := c.GetHeader("User-Agent")
exec.Command("echo", userAgent, ">>", "/var/log/ua.log").Run()
c.Next()
}
}
Here, a malicious User-Agent header could inject arbitrary commands.
Gin-Specific Detection
Detecting command injection in Gin applications requires both static code analysis and dynamic testing. For static analysis, look for these patterns:
// Dangerous: direct string concatenation
cmd := exec.Command("ls", "/tmp/"+userInput)
// Dangerous: using sh -c with user input
shCmd := exec.Command("sh", "-c", userInput)
// Dangerous: using os/exec with unvalidated paths
os.ReadFile(basePath + userInput)
middleBrick's API security scanner can automatically detect these patterns in your Gin endpoints. It tests for command injection by sending payloads like:
payloads := []string{
"; id",
"| whoami",
"&& ls /",
"; touch /tmp/proof",
"; cat /etc/passwd",
}
The scanner attempts to execute these through your API endpoints and checks for indicators like unexpected process execution, file creation, or information disclosure.
For Gin-specific detection, middleBrick analyzes:
- Handler functions that use
exec.Commandwith query parameters - Middleware that processes headers or body data into system commands
- File operations with path traversal possibilities
- Template rendering that might execute shell commands
middleBrick's LLM security module also checks for AI-specific command injection patterns if your Gin app uses language models, testing for jailbreak prompts that could execute system commands through the LLM interface.
Gin-Specific Remediation
Fixing command injection in Gin requires a defense-in-depth approach. The most effective remediation is avoiding system commands entirely when possible:
// Instead of using tar command
tar := exec.Command("tar", "czf", filepath, sourceDir)
// Use Go's archive/tar package directly
import "archive/tar"
func createTarball(c *gin.Context) {
filename := sanitizeFilename(c.Query("file"))
targetPath := filepath.Join("/backups", filename+"tar.gz")
f, err := os.Create(targetPath)
if err != nil { ... }
defer f.Close()
tw := tar.NewWriter(f)
defer tw.Close()
addFileToTar(tw, "/data")
c.JSON(200, gin.H{"status": "backup created"})
}
When system commands are unavoidable, use argument arrays instead of shell interpretation:
// Dangerous - shell interpretation
cmd := exec.Command("sh", "-c", "ls "+dir)
// Safe - argument array
cmd := exec.Command("ls", dir)
// Even safer - whitelist allowed directories
allowedDirs := map[string]string{
"tmp": "/tmp",
"logs": "/var/log",
}
func safeListDir(c *gin.Context) {
dirKey := c.Query("dir")
basePath, ok := allowedDirs[dirKey]
if !ok {
c.JSON(400, gin.H{"error": "invalid directory"})
return
}
cmd := exec.Command("ls", basePath)
output, _ := cmd.Output()
c.JSON(200, gin.H{"files": string(output)})
}
For file operations, use path sanitization and validation:
func sanitizeFilename(name string) string {
return filepath.Base(name)
}
func safeReadFile(c *gin.Context) {
userPath := c.Query("path")
cleanPath := filepath.Clean("/var/www" + userPath)
if !strings.HasPrefix(cleanPath, "/var/www") {
c.JSON(400, gin.H{"error": "invalid path"})
return
}
content, err := os.ReadFile(cleanPath)
if err != nil {
c.JSON(404, gin.H{"error": "file not found"})
return
}
c.String(200, string(content))
}
Gin's context binding can help validate input before it reaches dangerous operations:
type BackupRequest struct {
File string `json:"file" binding:"required,alphanum"`
Dir string `json:"dir" binding:"required"`
}
func backupHandler(c *gin.Context) {
var req BackupRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(400, gin.H{"error": "invalid input"})
return
}
// Now safe to use req.File and req.Dir
}
Related CWEs: inputValidation
| CWE ID | Name | Severity |
|---|---|---|
| CWE-20 | Improper Input Validation | HIGH |
| CWE-22 | Path Traversal | HIGH |
| CWE-74 | Injection | CRITICAL |
| CWE-77 | Command Injection | CRITICAL |
| CWE-78 | OS Command Injection | CRITICAL |
| CWE-79 | Cross-site Scripting (XSS) | HIGH |
| CWE-89 | SQL Injection | CRITICAL |
| CWE-90 | LDAP Injection | HIGH |
| CWE-91 | XML Injection | HIGH |
| CWE-94 | Code Injection | CRITICAL |
Frequently Asked Questions
How can I test my Gin application for command injection vulnerabilities?
; id, | whoami, or && ls / to endpoints that use exec.Command or file operations.Is using <code>exec.CommandContext</code> safer than <code>exec.Command</code>?
exec.CommandContext only adds timeout/cancellation capabilities. It doesn't prevent command injection if user input reaches the command. You still need proper input validation, argument arrays instead of shell interpretation, and avoiding system commands when possible.