Skip to content

Commit ea3a124

Browse files
authored
fix(python): add package name and version validation for requirements.txt files. (#6804)
1 parent a447f6b commit ea3a124

File tree

3 files changed

+77
-30
lines changed

3 files changed

+77
-30
lines changed

pkg/dependency/parser/python/pip/parse.go

+32-4
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,9 @@ import (
1010
"golang.org/x/text/transform"
1111
"golang.org/x/xerrors"
1212

13+
version "github.com/aquasecurity/go-pep440-version"
1314
ftypes "github.com/aquasecurity/trivy/pkg/fanal/types"
15+
"github.com/aquasecurity/trivy/pkg/log"
1416
xio "github.com/aquasecurity/trivy/pkg/x/io"
1517
)
1618

@@ -22,10 +24,14 @@ const (
2224
endExtras string = "]"
2325
)
2426

25-
type Parser struct{}
27+
type Parser struct {
28+
logger *log.Logger
29+
}
2630

2731
func NewParser() *Parser {
28-
return &Parser{}
32+
return &Parser{
33+
logger: log.WithPrefix("pip"),
34+
}
2935
}
3036

3137
func (p *Parser) Parse(r xio.ReadSeekerAt) ([]ftypes.Package, []ftypes.Dependency, error) {
@@ -40,8 +46,8 @@ func (p *Parser) Parse(r xio.ReadSeekerAt) ([]ftypes.Package, []ftypes.Dependenc
4046
var lineNumber int
4147
for scanner.Scan() {
4248
lineNumber++
43-
line := scanner.Text()
44-
line = strings.ReplaceAll(line, " ", "")
49+
text := scanner.Text()
50+
line := strings.ReplaceAll(text, " ", "")
4551
line = strings.ReplaceAll(line, `\`, "")
4652
line = removeExtras(line)
4753
line = rStripByKey(line, commentMarker)
@@ -51,6 +57,12 @@ func (p *Parser) Parse(r xio.ReadSeekerAt) ([]ftypes.Package, []ftypes.Dependenc
5157
if len(s) != 2 {
5258
continue
5359
}
60+
61+
if !isValidName(s[0]) || !isValidVersion(s[1]) {
62+
p.logger.Debug("Invalid package name/version in requirements.txt.", log.String("line", text))
63+
continue
64+
}
65+
5466
pkgs = append(pkgs, ftypes.Package{
5567
Name: s[0],
5668
Version: s[1],
@@ -83,3 +95,19 @@ func removeExtras(line string) string {
8395
}
8496
return line
8597
}
98+
99+
func isValidName(name string) bool {
100+
for _, r := range name {
101+
// only characters [A-Z0-9._-] are allowed (case insensitive)
102+
// cf. https://peps.python.org/pep-0508/#names
103+
if !unicode.IsLetter(r) && !unicode.IsDigit(r) && r != '.' && r != '_' && r != '-' {
104+
return false
105+
}
106+
}
107+
return true
108+
}
109+
110+
func isValidVersion(ver string) bool {
111+
_, err := version.Parse(ver)
112+
return err == nil
113+
}

pkg/dependency/parser/python/pip/parse_test.go

+40-26
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@ package pip
22

33
import (
44
"os"
5-
"path"
65
"testing"
76

87
"github.com/stretchr/testify/assert"
@@ -12,57 +11,72 @@ import (
1211
)
1312

1413
func TestParse(t *testing.T) {
15-
vectors := []struct {
16-
file string
17-
want []ftypes.Package
14+
tests := []struct {
15+
name string
16+
filePath string
17+
want []ftypes.Package
1818
}{
1919
{
20-
file: "testdata/requirements_flask.txt",
21-
want: requirementsFlask,
20+
name: "happy path",
21+
filePath: "testdata/requirements_flask.txt",
22+
want: requirementsFlask,
2223
},
2324
{
24-
file: "testdata/requirements_comments.txt",
25-
want: requirementsComments,
25+
name: "happy path with comments",
26+
filePath: "testdata/requirements_comments.txt",
27+
want: requirementsComments,
2628
},
2729
{
28-
file: "testdata/requirements_spaces.txt",
29-
want: requirementsSpaces,
30+
name: "happy path with spaces",
31+
filePath: "testdata/requirements_spaces.txt",
32+
want: requirementsSpaces,
3033
},
3134
{
32-
file: "testdata/requirements_no_version.txt",
33-
want: requirementsNoVersion,
35+
name: "happy path with dependency without version",
36+
filePath: "testdata/requirements_no_version.txt",
37+
want: requirementsNoVersion,
3438
},
3539
{
36-
file: "testdata/requirements_operator.txt",
37-
want: requirementsOperator,
40+
name: "happy path with operator",
41+
filePath: "testdata/requirements_operator.txt",
42+
want: requirementsOperator,
3843
},
3944
{
40-
file: "testdata/requirements_hash.txt",
41-
want: requirementsHash,
45+
name: "happy path with hash",
46+
filePath: "testdata/requirements_hash.txt",
47+
want: requirementsHash,
4248
},
4349
{
44-
file: "testdata/requirements_hyphens.txt",
45-
want: requirementsHyphens,
50+
name: "happy path with hyphens",
51+
filePath: "testdata/requirements_hyphens.txt",
52+
want: requirementsHyphens,
4653
},
4754
{
48-
file: "testdata/requirement_exstras.txt",
49-
want: requirementsExtras,
55+
name: "happy path with exstras",
56+
filePath: "testdata/requirement_exstras.txt",
57+
want: requirementsExtras,
5058
},
5159
{
52-
file: "testdata/requirements_utf16le.txt",
53-
want: requirementsUtf16le,
60+
name: "happy path. File uses utf16le",
61+
filePath: "testdata/requirements_utf16le.txt",
62+
want: requirementsUtf16le,
63+
},
64+
{
65+
name: "happy path with templating engine",
66+
filePath: "testdata/requirements_with_templating_engine.txt",
67+
want: nil,
5468
},
5569
}
5670

57-
for _, v := range vectors {
58-
t.Run(path.Base(v.file), func(t *testing.T) {
59-
f, err := os.Open(v.file)
71+
for _, tt := range tests {
72+
t.Run(tt.name, func(t *testing.T) {
73+
f, err := os.Open(tt.filePath)
6074
require.NoError(t, err)
6175

6276
got, _, err := NewParser().Parse(f)
6377
require.NoError(t, err)
6478

65-
assert.Equal(t, v.want, got)
79+
assert.Equal(t, tt.want, got)
6680
})
6781
}
6882
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
%ifcookiecutter.command_line_interface|lower=='click'-%}
2+
foo|bar=='1.2.4'
3+
foo{bar}==%%
4+
foo,bar&&foobar==1.2.3
5+
foo=='invalid-version'

0 commit comments

Comments
 (0)