diff --git a/README.md b/README.md index 75725a1..e1f58e4 100644 --- a/README.md +++ b/README.md @@ -257,6 +257,7 @@ directory you can supply `./...` as the input argument. - G704: SSRF via taint analysis - G705: XSS via taint analysis - G706: Log injection via taint analysis +- G707: SMTP command/header injection via taint analysis ### Retired rules diff --git a/analyzers/analyzers_test.go b/analyzers/analyzers_test.go index fbf31c6..6194a2a 100644 --- a/analyzers/analyzers_test.go +++ b/analyzers/analyzers_test.go @@ -118,5 +118,9 @@ var _ = Describe("gosec analyzers", func() { It("should detect log injection via taint analysis", func() { runner("G706", testutils.SampleCodeG706) }) + + It("should detect SMTP command/header injection via taint analysis", func() { + runner("G707", testutils.SampleCodeG707) + }) }) }) diff --git a/analyzers/analyzerslist.go b/analyzers/analyzerslist.go index c709179..658b30a 100644 --- a/analyzers/analyzerslist.go +++ b/analyzers/analyzerslist.go @@ -74,6 +74,13 @@ var ( Severity: "LOW", CWE: "CWE-117", } + + SMTPInjectionRule = taint.RuleInfo{ + ID: "G707", + Description: "SMTP command/header injection via user input", + Severity: "HIGH", + CWE: "CWE-93", + } ) // AnalyzerList contains a mapping of analyzer ID's to analyzer definitions and a mapping @@ -130,6 +137,7 @@ var defaultAnalyzers = []AnalyzerDefinition{ {"G704", "SSRF via taint analysis", newSSRFAnalyzer}, {"G705", "XSS via taint analysis", newXSSAnalyzer}, {"G706", "Log injection via taint analysis", newLogInjectionAnalyzer}, + {"G707", "SMTP command/header injection via taint analysis", newSMTPInjectionAnalyzer}, } // Generate the list of analyzers to use @@ -163,6 +171,7 @@ func DefaultTaintAnalyzers() []*analysis.Analyzer { ssrfConfig := SSRF() xssConfig := XSS() logConfig := LogInjection() + smtpConfig := SMTPInjection() return []*analysis.Analyzer{ taint.NewGosecAnalyzer(&SQLInjectionRule, &sqlConfig), @@ -171,5 +180,6 @@ func DefaultTaintAnalyzers() []*analysis.Analyzer { taint.NewGosecAnalyzer(&SSRFRule, &ssrfConfig), taint.NewGosecAnalyzer(&XSSRule, &xssConfig), taint.NewGosecAnalyzer(&LogInjectionRule, &logConfig), + taint.NewGosecAnalyzer(&SMTPInjectionRule, &smtpConfig), } } diff --git a/analyzers/analyzerslist_test.go b/analyzers/analyzerslist_test.go index ca1a827..b2395a0 100644 --- a/analyzers/analyzerslist_test.go +++ b/analyzers/analyzerslist_test.go @@ -62,6 +62,12 @@ func TestTaintAnalyzerConstructors(t *testing.T) { id: "G706", description: "Log injection via taint analysis", }, + { + name: "SMTPInjection", + constructor: newSMTPInjectionAnalyzer, + id: "G707", + description: "SMTP command/header injection via taint analysis", + }, } for _, tt := range tests { @@ -89,7 +95,7 @@ func TestTaintAnalyzerConstructors(t *testing.T) { // TestDefaultAnalyzersIncludeTaint tests that default analyzers include taint rules. func TestDefaultAnalyzersIncludeTaint(t *testing.T) { - expectedTaintIDs := []string{"G701", "G702", "G703", "G704", "G705", "G706"} + expectedTaintIDs := []string{"G701", "G702", "G703", "G704", "G705", "G706", "G707"} found := make(map[string]bool) for _, def := range defaultAnalyzers { @@ -107,7 +113,7 @@ func TestDefaultAnalyzersIncludeTaint(t *testing.T) { func TestGenerateIncludesTaintAnalyzers(t *testing.T) { analyzerList := Generate(false) - expectedTaintIDs := []string{"G701", "G702", "G703", "G704", "G705", "G706"} + expectedTaintIDs := []string{"G701", "G702", "G703", "G704", "G705", "G706", "G707"} for _, id := range expectedTaintIDs { if _, ok := analyzerList.Analyzers[id]; !ok { @@ -272,7 +278,7 @@ func TestAnalyzerList_AnalyzersInfo(t *testing.T) { func TestDefaultTaintAnalyzers(t *testing.T) { analyzers := DefaultTaintAnalyzers() - expectedCount := 6 // SQL, Command, Path, SSRF, XSS, Log + expectedCount := 7 // SQL, Command, Path, SSRF, XSS, Log, SMTP if len(analyzers) != expectedCount { t.Errorf("Expected %d taint analyzers, got %d", expectedCount, len(analyzers)) } @@ -284,6 +290,7 @@ func TestDefaultTaintAnalyzers(t *testing.T) { "G704": false, "G705": false, "G706": false, + "G707": false, } for _, analyzer := range analyzers { @@ -390,4 +397,13 @@ func TestTaintRuleConstants(t *testing.T) { t.Errorf("CWE = %s, want CWE-117", LogInjectionRule.CWE) } }) + + t.Run("SMTPInjection", func(t *testing.T) { + if SMTPInjectionRule.ID != "G707" { + t.Errorf("ID = %s, want G707", SMTPInjectionRule.ID) + } + if SMTPInjectionRule.CWE != "CWE-93" { + t.Errorf("CWE = %s, want CWE-93", SMTPInjectionRule.CWE) + } + }) } diff --git a/analyzers/smtpinjection.go b/analyzers/smtpinjection.go new file mode 100644 index 0000000..88fd8a3 --- /dev/null +++ b/analyzers/smtpinjection.go @@ -0,0 +1,67 @@ +// (c) Copyright gosec's authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package analyzers + +import ( + "golang.org/x/tools/go/analysis" + + "github.com/securego/gosec/v2/taint" +) + +// SMTPInjection returns a configuration for detecting SMTP command/header injection vulnerabilities. +func SMTPInjection() taint.Config { + return taint.Config{ + Sources: []taint.Source{ + // Type sources: tainted when received as parameters + {Package: "net/http", Name: "Request", Pointer: true}, + {Package: "net/url", Name: "URL", Pointer: true}, + {Package: "net/url", Name: "Values"}, + {Package: "bufio", Name: "Reader", Pointer: true}, + {Package: "bufio", Name: "Scanner", Pointer: true}, + + // Function sources + {Package: "os", Name: "Args", IsFunc: true}, + {Package: "os", Name: "Getenv", IsFunc: true}, + }, + Sinks: []taint.Sink{ + // net/smtp.SendMail(addr, auth, from, to, msg) + // Check sender and recipient envelope fields. + {Package: "net/smtp", Method: "SendMail", CheckArgs: []int{2, 3}}, + + // For smtp.Client methods, Args[0] is receiver. + {Package: "net/smtp", Receiver: "Client", Method: "Mail", Pointer: true, CheckArgs: []int{1}}, + {Package: "net/smtp", Receiver: "Client", Method: "Rcpt", Pointer: true, CheckArgs: []int{1}}, + }, + Sanitizers: []taint.Sanitizer{ + // net/mail parsers enforce RFC-compatible mailbox/address syntax. + {Package: "net/mail", Method: "ParseAddress"}, + {Package: "net/mail", Method: "ParseAddressList"}, + + // AddressParser methods also provide structured parsing. + {Package: "net/mail", Receiver: "AddressParser", Method: "Parse", Pointer: true}, + {Package: "net/mail", Receiver: "AddressParser", Method: "ParseList", Pointer: true}, + }, + } +} + +// newSMTPInjectionAnalyzer creates an analyzer for detecting SMTP injection vulnerabilities +// via taint analysis (G707) +func newSMTPInjectionAnalyzer(id string, description string) *analysis.Analyzer { + config := SMTPInjection() + rule := SMTPInjectionRule + rule.ID = id + rule.Description = description + return taint.NewGosecAnalyzer(&rule, &config) +} diff --git a/cwe/data.go b/cwe/data.go index 13f5461..c8bfc27 100644 --- a/cwe/data.go +++ b/cwe/data.go @@ -43,6 +43,11 @@ var idWeaknesses = map[string]*Weakness{ Description: "The software constructs all or part of an SQL command using externally-influenced input from an upstream component, but it does not neutralize or incorrectly neutralizes special elements that could modify the intended SQL command when it is sent to a downstream component.", Name: "Improper Neutralization of Special Elements used in an SQL Command ('SQL Injection')", }, + "93": { + ID: "93", + Description: "The software does not properly neutralize CRLF sequences before using externally-influenced input in protocol elements that rely on CRLF as delimiters, allowing attackers to inject additional commands or headers.", + Name: "Improper Neutralization of CRLF Sequences ('CRLF Injection')", + }, "118": { ID: "118", Description: "The software does not restrict or incorrectly restricts operations within the boundaries of a resource that is accessed using an index or pointer, such as memory or files.", diff --git a/issue/issue.go b/issue/issue.go index eaeb776..c0d485f 100644 --- a/issue/issue.go +++ b/issue/issue.go @@ -65,6 +65,7 @@ var ruleToCWE = map[string]string{ "G110": "409", "G111": "22", "G112": "400", + "G707": "93", "G114": "676", "G115": "190", "G116": "838", diff --git a/testutils/g707_samples.go b/testutils/g707_samples.go new file mode 100644 index 0000000..7aec85f --- /dev/null +++ b/testutils/g707_samples.go @@ -0,0 +1,76 @@ +package testutils + +import "github.com/securego/gosec/v2" + +// SampleCodeG707 - SMTP command/header injection via taint analysis +var SampleCodeG707 = []CodeSample{ + {[]string{` +package main + +import ( + "net/http" + "net/smtp" +) + +func handler(r *http.Request) { + from := r.FormValue("from") + to := []string{r.FormValue("to")} + _ = smtp.SendMail("127.0.0.1:25", nil, from, to, []byte("Subject: Hi\r\n\r\nbody")) +} +`}, 1, gosec.NewConfig()}, + {[]string{` +package main + +import ( + "net/http" + "net/smtp" +) + +func handler(r *http.Request, c *smtp.Client) { + from := r.URL.Query().Get("from") + to := r.URL.Query().Get("to") + _ = c.Mail(from) + _ = c.Rcpt(to) +} +`}, 2, gosec.NewConfig()}, + {[]string{` +package main + +import ( + "net/http" + "net/mail" + "net/smtp" +) + +func handler(r *http.Request) { + parsed, err := mail.ParseAddress(r.FormValue("from")) + if err != nil { + return + } + _ = smtp.SendMail("127.0.0.1:25", nil, parsed.Address, []string{"recipient@example.com"}, []byte("Subject: Hi\r\n\r\nbody")) +} +`}, 0, gosec.NewConfig()}, + {[]string{` +package main + +import ( + "net/http" + "net/mail" + "net/smtp" +) + +func handler(r *http.Request) { + addresses, err := mail.ParseAddressList(r.FormValue("to")) + if err != nil { + return + } + + recipients := make([]string, 0, len(addresses)) + for _, addr := range addresses { + recipients = append(recipients, addr.Address) + } + + _ = smtp.SendMail("127.0.0.1:25", nil, "sender@example.com", recipients, []byte("Subject: Hi\r\n\r\nbody")) +} +`}, 0, gosec.NewConfig()}, +}