Skip to content

Commit 398ad54

Browse files
authored
feat: Support for adding taint analysis engine (#1486)
* feat: add taint analysis engine for data flow security Implements SSA-based taint analysis to detect security vulnerabilities: - G701: SQL injection via string concatenation - G702: Command injection via user input - G703: Path traversal via user input - G704: SSRF via user-controlled URLs - G705: XSS via unescaped user input - G706: Log injection via user input Uses golang.org/x/tools for SSA/call graph analysis with CHA. Zero external dependencies beyond existing gosec imports.
1 parent 6eacd5c commit 398ad54

24 files changed

+1868
-29
lines changed

README.md

Lines changed: 150 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,12 @@ Inspects source code for security problems by scanning the Go AST and SSA code r
55

66
<img src="https://securego.io/img/gosec.png" width="320">
77

8+
## Features
9+
10+
- **Pattern-based rules** for detecting common security issues in Go code
11+
- **SSA-based analyzers** for type conversions, slice bounds, and crypto issues
12+
- **Taint analysis** for tracking data flow from user input to dangerous functions (SQL injection, command injection, path traversal, SSRF, XSS, log injection)
13+
814
## License
915

1016
Licensed under the Apache License, Version 2.0 (the "License").
@@ -216,6 +222,12 @@ directory you can supply `./...` as the input argument.
216222
- G507: Import blocklist: golang.org/x/crypto/ripemd160
217223
- G601: Implicit memory aliasing of items from a range statement (only for Go 1.21 or lower)
218224
- G602: Slice access out of bounds
225+
- G701: SQL injection via taint analysis
226+
- G702: Command injection via taint analysis
227+
- G703: Path traversal via taint analysis
228+
- G704: SSRF via taint analysis
229+
- G705: XSS via taint analysis
230+
- G706: Log injection via taint analysis
219231

220232
### Retired rules
221233

@@ -498,6 +510,144 @@ $ gosec -fmt=json -out=results.json -stdout -verbose=text *.go
498510

499511
[CONTRIBUTING.md](https://github.com/securego/gosec/blob/master/CONTRIBUTING.md) contains detailed information about adding new rules to gosec.
500512

513+
### Creating Taint Analysis Rules
514+
515+
gosec supports taint analysis to track data flow from untrusted sources (user input) to dangerous sinks (functions that could cause security vulnerabilities). The taint analysis rules detect issues like SQL injection, command injection, path traversal, SSRF, XSS, and log injection.
516+
517+
#### Creating a New Taint Rule
518+
519+
To create a new taint analysis rule:
520+
521+
1. **Create the analyzer file** in `analyzers/` (e.g., `analyzers/newvuln.go`) with both the configuration and analyzer:
522+
523+
```go
524+
package analyzers
525+
526+
import (
527+
"golang.org/x/tools/go/analysis"
528+
"github.com/securego/gosec/v2/taint"
529+
)
530+
531+
// NewVulnerability returns a configuration for detecting your vulnerability
532+
func NewVulnerability() taint.Config {
533+
return taint.Config{
534+
Sources: []taint.Source{
535+
{Package: "net/http", Name: "Request", Pointer: true},
536+
{Package: "os", Name: "Args"},
537+
},
538+
Sinks: []taint.Sink{
539+
{Package: "dangerous/package", Method: "DangerousFunc"},
540+
{Package: "another/pkg", Receiver: "Type", Method: "Method", Pointer: true},
541+
{Package: "database/sql", Receiver: "DB", Method: "Query", Pointer: true, CheckArgs: []int{1}},
542+
},
543+
}
544+
}
545+
546+
func newNewVulnAnalyzer(id string, description string) *analysis.Analyzer {
547+
config := NewVulnerability()
548+
rule := NewVulnerabilityRule // Define this as a variable in the same file
549+
rule.ID = id
550+
rule.Description = description
551+
return taint.NewGosecAnalyzer(&rule, &config)
552+
}
553+
```
554+
555+
**Note**: Each taint analyzer keeps its configuration function in the same file as the analyzer. For examples, see:
556+
- `analyzers/sqlinjection.go` - SQL injection detection (G701)
557+
- `analyzers/commandinjection.go` - Command injection detection (G702)
558+
- `analyzers/pathtraversal.go` - Path traversal detection (G703)
559+
560+
2. **Register the analyzer** in `analyzers/analyzerslist.go`:
561+
562+
```go
563+
var defaultAnalyzers = []AnalyzerDefinition{
564+
// ... existing analyzers ...
565+
{"G7XX", "Description of vulnerability", newNewVulnAnalyzer},
566+
}
567+
```
568+
569+
3. **Add test samples** in `testutils/g7xx_samples.go`:
570+
571+
```go
572+
package testutils
573+
574+
import "github.com/securego/gosec/v2"
575+
576+
// SampleCodeG7XX - Description of vulnerability
577+
var SampleCodeG7XX = []CodeSample{
578+
{[]string{`
579+
package main
580+
581+
import (
582+
"dangerous/package"
583+
"net/http"
584+
)
585+
586+
func handler(r *http.Request) {
587+
input := r.URL.Query().Get("param")
588+
dangerous.DangerousFunc(input) // Should detect
589+
}
590+
`}, 1, gosec.NewConfig()},
591+
{[]string{`
592+
package main
593+
594+
import (
595+
"dangerous/package"
596+
)
597+
598+
func safeHandler() {
599+
// Safe - no user input
600+
dangerous.DangerousFunc("constant")
601+
}
602+
`}, 0, gosec.NewConfig()},
603+
}
604+
```
605+
606+
Then add the test case to `analyzers/analyzers_test.go`:
607+
608+
```go
609+
It("should detect your new vulnerability", func() {
610+
runner("G7XX", testutils.SampleCodeG7XX)
611+
})
612+
```
613+
614+
#### Source and Sink Configuration
615+
616+
**Sources** define where tainted (untrusted) data originates:
617+
- `Package`: The import path (e.g., `"net/http"`)
618+
- `Name`: The type or function name (e.g., `"Request"`)
619+
- `Pointer`: Set to `true` if it's a pointer type (e.g., `*http.Request`)
620+
621+
**Sinks** define dangerous functions that should not receive tainted data:
622+
- `Package`: The import path (e.g., `"database/sql"`)
623+
- `Receiver`: The type name for methods (e.g., `"DB"`), or empty for package functions
624+
- `Method`: The function or method name (e.g., `"Query"`)
625+
- `Pointer`: Set to `true` if the receiver is a pointer type
626+
- `CheckArgs`: Optional list of argument indices to check (e.g., `[]int{1}` to check only the second argument). If omitted, all arguments are checked. Useful when some arguments are safe (like prepared statement parameters) or should be ignored (like writer arguments in `fmt.Fprintf`)
627+
628+
**Example with CheckArgs:**
629+
```go
630+
// For SQL methods, Args[0] is the receiver (*sql.DB), Args[1] is the query string
631+
// Only check the query string; prepared statement parameters (Args[2+]) are safe
632+
{Package: "database/sql", Receiver: "DB", Method: "Query", Pointer: true, CheckArgs: []int{1}}
633+
634+
// For fmt.Fprintf, Args[0] is the writer (os.Stderr), Args[1+] are format and data
635+
// Skip the writer argument, only check format string and data arguments
636+
{Package: "fmt", Method: "Fprintf", CheckArgs: []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}}
637+
```
638+
639+
#### Common Taint Sources
640+
641+
| Source Type | Package | Type/Method | Pointer |
642+
|-------------|---------|-------------|---------|
643+
| HTTP Request | `net/http` | `Request` | `true` |
644+
| Query Parameters | `net/http` | `Request.URL.Query()` | - |
645+
| Form Data | `net/http` | `Request.FormValue()` | - |
646+
| Headers | `net/http` | `Request.Header` | - |
647+
| Command Line Args | `os` | `Args` | `false` |
648+
| Environment Variables | `os` | `Getenv` | `false` |
649+
| File Content | `bufio` | `Reader` | `true` |
650+
501651
### Build
502652

503653
You can build the binary with:

analyzer.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ import (
4141
"golang.org/x/tools/go/packages"
4242

4343
"github.com/securego/gosec/v2/analyzers"
44+
"github.com/securego/gosec/v2/internal/ssautil"
4445
"github.com/securego/gosec/v2/issue"
4546
)
4647

@@ -555,7 +556,7 @@ func (gosec *Analyzer) CheckAnalyzersWithSSA(pkg *packages.Package, ssaResult *b
555556
// checkAnalyzersWithSSA runs analyzers on a given package using an existing SSA result (Stateless API).
556557
func (gosec *Analyzer) checkAnalyzersWithSSA(pkg *packages.Package, ssaResult *buildssa.SSA, allIgnores ignores) ([]*issue.Issue, *Metrics) {
557558
resultMap := map[*analysis.Analyzer]any{
558-
buildssa.Analyzer: &analyzers.SSAAnalyzerResult{
559+
buildssa.Analyzer: &ssautil.SSAAnalyzerResult{
559560
Config: gosec.Config(),
560561
Logger: gosec.logger,
561562
SSA: ssaResult,

analyzers/analyzers_test.go

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,5 +62,29 @@ var _ = Describe("gosec analyzers", func() {
6262
It("should detect out of bounds slice access", func() {
6363
runner("G602", testutils.SampleCodeG602)
6464
})
65+
66+
It("should detect SQL injection via taint analysis", func() {
67+
runner("G701", testutils.SampleCodeG701)
68+
})
69+
70+
It("should detect command injection via taint analysis", func() {
71+
runner("G702", testutils.SampleCodeG702)
72+
})
73+
74+
It("should detect path traversal via taint analysis", func() {
75+
runner("G703", testutils.SampleCodeG703)
76+
})
77+
78+
It("should detect SSRF via taint analysis", func() {
79+
runner("G704", testutils.SampleCodeG704)
80+
})
81+
82+
It("should detect XSS via taint analysis", func() {
83+
runner("G705", testutils.SampleCodeG705)
84+
})
85+
86+
It("should detect log injection via taint analysis", func() {
87+
runner("G706", testutils.SampleCodeG706)
88+
})
6589
})
6690
})

analyzers/analyzerslist.go

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@ package analyzers
1616

1717
import (
1818
"golang.org/x/tools/go/analysis"
19+
20+
"github.com/securego/gosec/v2/taint"
1921
)
2022

2123
// AnalyzerDefinition contains the description of an analyzer and a mechanism to
@@ -29,6 +31,51 @@ type AnalyzerDefinition struct {
2931
// AnalyzerBuilder is used to register an analyzer definition with the analyzer
3032
type AnalyzerBuilder func(id string, description string) *analysis.Analyzer
3133

34+
// Taint analysis rule definitions
35+
var (
36+
SQLInjectionRule = taint.RuleInfo{
37+
ID: "G701",
38+
Description: "SQL injection via string concatenation",
39+
Severity: "HIGH",
40+
CWE: "CWE-89",
41+
}
42+
43+
CommandInjectionRule = taint.RuleInfo{
44+
ID: "G702",
45+
Description: "Command injection via user input",
46+
Severity: "CRITICAL",
47+
CWE: "CWE-78",
48+
}
49+
50+
PathTraversalRule = taint.RuleInfo{
51+
ID: "G703",
52+
Description: "Path traversal via user input",
53+
Severity: "HIGH",
54+
CWE: "CWE-22",
55+
}
56+
57+
SSRFRule = taint.RuleInfo{
58+
ID: "G704",
59+
Description: "SSRF via user-controlled URL",
60+
Severity: "HIGH",
61+
CWE: "CWE-918",
62+
}
63+
64+
XSSRule = taint.RuleInfo{
65+
ID: "G705",
66+
Description: "XSS via unescaped user input",
67+
Severity: "MEDIUM",
68+
CWE: "CWE-79",
69+
}
70+
71+
LogInjectionRule = taint.RuleInfo{
72+
ID: "G706",
73+
Description: "Log injection via user input",
74+
Severity: "LOW",
75+
CWE: "CWE-117",
76+
}
77+
)
78+
3279
// AnalyzerList contains a mapping of analyzer ID's to analyzer definitions and a mapping
3380
// of analyzer ID's to whether analyzers are suppressed.
3481
type AnalyzerList struct {
@@ -69,6 +116,12 @@ var defaultAnalyzers = []AnalyzerDefinition{
69116
{"G115", "Type conversion which leads to integer overflow", newConversionOverflowAnalyzer},
70117
{"G602", "Possible slice bounds out of range", newSliceBoundsAnalyzer},
71118
{"G407", "Use of hardcoded IV/nonce for encryption", newHardCodedNonce},
119+
{"G701", "SQL injection via taint analysis", newSQLInjectionAnalyzer},
120+
{"G702", "Command injection via taint analysis", newCommandInjectionAnalyzer},
121+
{"G703", "Path traversal via taint analysis", newPathTraversalAnalyzer},
122+
{"G704", "SSRF via taint analysis", newSSRFAnalyzer},
123+
{"G705", "XSS via taint analysis", newXSSAnalyzer},
124+
{"G706", "Log injection via taint analysis", newLogInjectionAnalyzer},
72125
}
73126

74127
// Generate the list of analyzers to use
@@ -93,3 +146,22 @@ func Generate(trackSuppressions bool, filters ...AnalyzerFilter) *AnalyzerList {
93146
}
94147
return &AnalyzerList{Analyzers: analyzerMap, AnalyzerSuppressed: analyzerSuppressedMap}
95148
}
149+
150+
// DefaultTaintAnalyzers returns all predefined taint analysis analyzers.
151+
func DefaultTaintAnalyzers() []*analysis.Analyzer {
152+
sqlConfig := SQLInjection()
153+
cmdConfig := CommandInjection()
154+
pathConfig := PathTraversal()
155+
ssrfConfig := SSRF()
156+
xssConfig := XSS()
157+
logConfig := LogInjection()
158+
159+
return []*analysis.Analyzer{
160+
taint.NewGosecAnalyzer(&SQLInjectionRule, &sqlConfig),
161+
taint.NewGosecAnalyzer(&CommandInjectionRule, &cmdConfig),
162+
taint.NewGosecAnalyzer(&PathTraversalRule, &pathConfig),
163+
taint.NewGosecAnalyzer(&SSRFRule, &ssrfConfig),
164+
taint.NewGosecAnalyzer(&XSSRule, &xssConfig),
165+
taint.NewGosecAnalyzer(&LogInjectionRule, &logConfig),
166+
}
167+
}

0 commit comments

Comments
 (0)