Skip to content

Commit 820fd2e

Browse files
committed
fix: address fuzzing issues
Signed-off-by: egibs <20933572+egibs@users.noreply.github.com>
1 parent 2bf11c5 commit 820fd2e

10 files changed

Lines changed: 113 additions & 8 deletions

File tree

pkg/archive/archive.go

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -331,9 +331,19 @@ func handleSymlink(dir, linkPath, linkTarget string) error {
331331
return nil
332332
}
333333

334+
parentDir := filepath.Dir(fullPath)
335+
resolvedDir := dir
336+
if rp, err := filepath.EvalSymlinks(parentDir); err == nil {
337+
parentDir = rp
338+
if rd, err := filepath.EvalSymlinks(dir); err == nil {
339+
resolvedDir = rd
340+
}
341+
}
342+
334343
// Validate relative symlink target resolves within extraction directory
335-
resolvedTarget := filepath.Clean(filepath.Join(filepath.Dir(fullPath), linkTarget))
336-
if !IsValidPath(resolvedTarget, dir) {
344+
// using the actual (resolved) parent directory
345+
resolvedTarget := filepath.Clean(filepath.Join(parentDir, linkTarget))
346+
if !IsValidPath(resolvedTarget, resolvedDir) {
337347
return fmt.Errorf("symlink target escapes extraction directory: %s -> %s", linkPath, linkTarget)
338348
}
339349

@@ -363,8 +373,9 @@ func handleSymlink(dir, linkPath, linkTarget string) error {
363373
return fmt.Errorf("symlink target mismatch: expected %s, got %s", linkTarget, actualTarget)
364374
}
365375

366-
actualResolved := filepath.Clean(filepath.Join(filepath.Dir(fullPath), actualTarget))
367-
if !IsValidPath(actualResolved, dir) {
376+
// Post-creation validation using the resolved parent directory
377+
actualResolved := filepath.Clean(filepath.Join(parentDir, actualTarget))
378+
if !IsValidPath(actualResolved, resolvedDir) {
368379
os.Remove(fullPath)
369380
return fmt.Errorf("symlink target escapes extraction directory after creation: %s -> %s", linkPath, actualTarget)
370381
}

pkg/archive/deb.go

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,16 @@ func ExtractDeb(ctx context.Context, d, f string) error {
5757
return fmt.Errorf("invalid file path: %s", target)
5858
}
5959

60+
if resolvedParent, err := filepath.EvalSymlinks(filepath.Dir(target)); err == nil {
61+
resolvedDir, dirErr := filepath.EvalSymlinks(d)
62+
if dirErr == nil {
63+
resolvedTarget := filepath.Join(resolvedParent, filepath.Base(target))
64+
if !IsValidPath(resolvedTarget, resolvedDir) {
65+
return fmt.Errorf("path traversal via symlink in parent directory: %s", clean)
66+
}
67+
}
68+
}
69+
6070
switch header.Typeflag {
6171
case tar.TypeDir:
6272
if err := handleDirectory(target); err != nil {

pkg/archive/fuzz_test.go

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
package archive
55

66
import (
7+
"bytes"
78
"context"
89
"os"
910
"path/filepath"
@@ -477,11 +478,17 @@ func FuzzExtractRPM(f *testing.F) {
477478
}
478479
}
479480

480-
f.Add([]byte{}) // empty
481-
f.Add([]byte{0xed, 0xab, 0xee, 0xdb}) // rpm magic
482-
f.Add([]byte("not rpm")) // invalid
481+
rpmMagic := []byte{0xed, 0xab, 0xee, 0xdb}
482+
483+
f.Add([]byte{}) // empty
484+
f.Add(rpmMagic) // rpm magic
485+
f.Add([]byte("not rpm")) // invalid
483486

484487
f.Fuzz(func(t *testing.T, data []byte) {
488+
if len(data) < 96 || !bytes.Equal(data[:4], rpmMagic) {
489+
return
490+
}
491+
485492
tmpFile, err := os.CreateTemp("", "fuzz-rpm-*.rpm")
486493
if err != nil {
487494
t.Skip()

pkg/archive/rpm.go

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -153,6 +153,16 @@ func ExtractRPM(ctx context.Context, d, f string) error {
153153
return fmt.Errorf("invalid file path: %s", target)
154154
}
155155

156+
if resolvedParent, err := filepath.EvalSymlinks(filepath.Dir(target)); err == nil {
157+
resolvedDir, dirErr := filepath.EvalSymlinks(d)
158+
if dirErr == nil {
159+
resolvedTarget := filepath.Join(resolvedParent, filepath.Base(target))
160+
if !IsValidPath(resolvedTarget, resolvedDir) {
161+
return fmt.Errorf("path traversal via symlink in parent directory: %s", clean)
162+
}
163+
}
164+
}
165+
156166
// https://github.com/cavaliergopher/cpio/blob/main/header.go#L24
157167
const modeTypeMask = 0o170000
158168
fileType := header.Mode & modeTypeMask

pkg/archive/tar.go

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -189,6 +189,16 @@ func ExtractTar(ctx context.Context, d string, f string) error {
189189
return fmt.Errorf("invalid file path: %s", target)
190190
}
191191

192+
if resolvedParent, err := filepath.EvalSymlinks(filepath.Dir(target)); err == nil {
193+
resolvedDir, dirErr := filepath.EvalSymlinks(d)
194+
if dirErr == nil {
195+
resolvedTarget := filepath.Join(resolvedParent, filepath.Base(target))
196+
if !IsValidPath(resolvedTarget, resolvedDir) {
197+
return fmt.Errorf("path traversal via symlink in parent directory: %s", clean)
198+
}
199+
}
200+
}
201+
192202
switch header.Typeflag {
193203
case tar.TypeDir:
194204
if err := handleDirectory(target); err != nil {

pkg/archive/zip.go

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -131,6 +131,17 @@ func extractFile(ctx context.Context, zf *zip.File, destDir string, logger *clog
131131
return nil
132132
}
133133

134+
if resolvedParent, err := filepath.EvalSymlinks(filepath.Dir(target)); err == nil {
135+
resolvedDir, dirErr := filepath.EvalSymlinks(destDir)
136+
if dirErr == nil {
137+
resolvedTarget := filepath.Join(resolvedParent, filepath.Base(target))
138+
if !IsValidPath(resolvedTarget, resolvedDir) {
139+
logger.Warnf("skipping path with symlink traversal: %s", target)
140+
return nil
141+
}
142+
}
143+
}
144+
134145
if zf.Mode()&os.ModeSymlink != 0 {
135146
src, err := zf.Open()
136147
if err != nil {

pkg/compile/fuzz_test.go

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
package compile
55

66
import (
7+
"bytes"
78
"context"
89
"io/fs"
910
"regexp"
@@ -131,7 +132,21 @@ func FuzzRecursiveCompile(f *testing.F) {
131132
// Non-UTF8 rule name (should be skipped)
132133
f.Add([]byte(`rule \xff\xfe { condition: true }`))
133134

135+
// yara[-x] does not support floating-point literals in conditions.
136+
// The yara-x C API aborts (exit status 2) on inputs like
137+
// "filesize < 1.485760", which cannot be caught by recover().
138+
floatPattern := regexp.MustCompile(`\d+\.\d+`)
139+
134140
f.Fuzz(func(_ *testing.T, data []byte) {
141+
if len(data) > 50*1024*1024 {
142+
return
143+
}
144+
145+
// Skip inputs with float literals that crash the YARA-X C library.
146+
if bytes.Contains(data, []byte("condition")) && floatPattern.Match(data) {
147+
return
148+
}
149+
135150
fsys := fstest.MapFS{
136151
"test.yara": {
137152
Data: data,
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
go test fuzz v1
2+
[]byte("rule cxFreeze_Python_executable: high {\n meta:\n description = \"uses cxFreeze packer\"\n filetypes = \"py\"\n\n strings:\n $cxfreeze = \"cx_Freeze\"\n $not_importlib = \"tool like cx_Freeze\"\n\n condition:\n filesize < 1.485760 and $cxfreeze and none of ($not*)\n}\n")

pkg/programkind/fuzz_test.go

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,9 @@ import (
1313

1414
// FuzzFile tests file type detection with random inputs.
1515
func FuzzFile(f *testing.F) {
16+
// Limit seed file size to avoid excessive memory usage during fuzzing.
17+
const maxSeedSize int64 = 50 * 1024 * 1024 // 50MB
18+
1619
samplesDir := "../../out/chainguard-sandbox/malcontent-samples"
1720
err := filepath.WalkDir(samplesDir, func(path string, d os.DirEntry, _ error) error {
1821
if d == nil || d.IsDir() {
@@ -22,6 +25,14 @@ func FuzzFile(f *testing.F) {
2225
return nil
2326
}
2427

28+
info, infoErr := d.Info()
29+
if infoErr != nil {
30+
return infoErr
31+
}
32+
if info.Size() > maxSeedSize {
33+
return nil
34+
}
35+
2536
if data, readErr := os.ReadFile(path); readErr == nil {
2637
f.Add(data, filepath.Base(path))
2738
}
@@ -47,6 +58,9 @@ func FuzzFile(f *testing.F) {
4758
f.Add([]byte("UPX!"), "test.upx") // UPX magic
4859

4960
f.Fuzz(func(t *testing.T, data []byte, filename string) {
61+
if len(data) > 50*1024*1024 {
62+
return
63+
}
5064
if len(filename) > 255 || filepath.Clean(filename) != filename {
5165
return
5266
}

pkg/render/fuzz_test.go

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,8 +31,23 @@ func FuzzRenderDifferential(f *testing.F) {
3131
f.Add(int8(0), "/very/long/"+strings.Repeat("path/", 50), "behavior", "description", false)
3232
f.Add(int8(4), "/bin/app", "critical", "Very dangerous", true) // With diff
3333

34+
// YAML special values that cannot round-trip as map keys due to
35+
// YAML 1.1 merge key and implicit typing (boolean, null) semantics.
36+
yamlIgnore := map[string]bool{
37+
"<<": true, "~": true,
38+
"null": true, "Null": true, "NULL": true,
39+
"true": true, "True": true, "TRUE": true,
40+
"false": true, "False": true, "FALSE": true,
41+
"yes": true, "Yes": true, "YES": true,
42+
"no": true, "No": true, "NO": true,
43+
"on": true, "On": true, "ON": true,
44+
"off": true, "Off": true, "OFF": true,
45+
"y": true, "Y": true,
46+
"n": true, "N": true,
47+
}
48+
3449
f.Fuzz(func(t *testing.T, riskLevel int8, filePath, behaviorName, behaviorDesc string, hasDiff bool) {
35-
if filePath == "" {
50+
if filePath == "" || yamlIgnore[filePath] {
3651
return
3752
}
3853

0 commit comments

Comments
 (0)