Skip to content

Commit 3625d88

Browse files
committed
✅ test: add coverage for Ruby license fetching and path traversal
- Add unit tests for `fetchInstalledLicense` in `pkg/deps/ruby_test.go` covering: - Multiple versions of the same gem - Invalid version strings - Missing gemspec files - Gems with similar names - Add unit tests for path traversal protection in `pkg/deps/ruby_test.go` covering: - Path traversal attempts in `Gemfile.lock` - Fix `fetchInstalledLicense` in `pkg/deps/ruby.go` to return empty string for invalid version strings instead of falling back to any installed version.
1 parent 98631c0 commit 3625d88

File tree

2 files changed

+133
-0
lines changed

2 files changed

+133
-0
lines changed

pkg/deps/ruby.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -540,6 +540,9 @@ func fetchLocalLicense(dir, targetName string) (string, error) {
540540
}
541541

542542
func fetchInstalledLicense(name, version string) string {
543+
if version != "" && !rubyVersionRe.MatchString(version) {
544+
return ""
545+
}
543546
gems := getAllGemspecs()
544547
for _, path := range gems {
545548
filename := filepath.Base(path)

pkg/deps/ruby_test.go

Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ package deps
2020
import (
2121
"bufio"
2222
"embed"
23+
"fmt"
2324
"io"
2425
"io/fs"
2526
"net/http"
@@ -353,3 +354,132 @@ func TestGemspecIgnoresCommentedRuntimeDependencies(t *testing.T) {
353354
t.Fatalf("expected 0 dependencies when runtime deps are only commented, got %d", got)
354355
}
355356
}
357+
358+
func TestFetchInstalledLicense(t *testing.T) {
359+
gemHome := t.TempDir()
360+
specsDir := filepath.Join(gemHome, "specifications")
361+
if err := os.MkdirAll(specsDir, 0755); err != nil {
362+
t.Fatal(err)
363+
}
364+
365+
createGemspec := func(filename, name, version, license string) {
366+
content := fmt.Sprintf(`
367+
Gem::Specification.new do |s|
368+
s.name = "%s"
369+
s.version = "%s"
370+
s.licenses = ["%s"]
371+
end
372+
`, name, version, license)
373+
if err := os.WriteFile(filepath.Join(specsDir, filename), []byte(content), 0644); err != nil {
374+
t.Fatal(err)
375+
}
376+
}
377+
378+
createGemspec("foo-1.0.0.gemspec", "foo", "1.0.0", "MIT")
379+
createGemspec("foo-2.0.0.gemspec", "foo", "2.0.0", "GPL-3.0")
380+
createGemspec("foo-bar-1.0.0.gemspec", "foo-bar", "1.0.0", "Apache-2.0")
381+
createGemspec("bar-1.0.0.gemspec", "bar", "1.0.0", "BSD-3-Clause")
382+
// Invalid version string in filename
383+
createGemspec("foo-invalid.gemspec", "foo", "invalid", "WTFPL")
384+
385+
t.Setenv("GEM_HOME", gemHome)
386+
387+
tests := []struct {
388+
name string
389+
version string
390+
want string
391+
}{
392+
{"foo", "1.0.0", "MIT"},
393+
{"foo", "2.0.0", "GPL-3.0"},
394+
{"foo-bar", "1.0.0", "Apache-2.0"},
395+
{"bar", "1.0.0", "BSD-3-Clause"},
396+
{"foo", "", "MIT"}, // Should find first available version (1.0.0 comes before 2.0.0)
397+
{"foo", "3.0.0", ""}, // Not found
398+
{"unknown", "1.0.0", ""},
399+
{"foo", "invalid", ""}, // Invalid version requested, regex won't match
400+
}
401+
402+
for _, tt := range tests {
403+
t.Run(fmt.Sprintf("%s-%s", tt.name, tt.version), func(t *testing.T) {
404+
got := fetchInstalledLicense(tt.name, tt.version)
405+
if got != tt.want {
406+
t.Errorf("fetchInstalledLicense(%q, %q) = %q, want %q", tt.name, tt.version, got, tt.want)
407+
}
408+
})
409+
}
410+
}
411+
412+
func TestRubyGemfileLockResolver_PathTraversal(t *testing.T) {
413+
resolver := new(GemfileLockResolver)
414+
dir := t.TempDir()
415+
416+
// Create a directory outside the project
417+
outsideDir := t.TempDir()
418+
// Create a gemspec in outsideDir
419+
outsideGemspec := filepath.Join(outsideDir, "evil.gemspec")
420+
if err := os.WriteFile(outsideGemspec, []byte(`
421+
Gem::Specification.new do |s|
422+
s.name = "evil"
423+
s.version = "1.0.0"
424+
s.licenses = ["Evil-License"]
425+
end
426+
`), 0644); err != nil {
427+
t.Fatal(err)
428+
}
429+
430+
// Calculate relative path from dir to outsideDir
431+
relPath, err := filepath.Rel(dir, outsideDir)
432+
if err != nil {
433+
t.Fatal(err)
434+
}
435+
436+
lockContent := fmt.Sprintf(`
437+
PATH
438+
remote: %s
439+
specs:
440+
evil (1.0.0)
441+
442+
GEM
443+
remote: https://gem.coop/
444+
specs:
445+
446+
PLATFORMS
447+
ruby
448+
449+
DEPENDENCIES
450+
evil!
451+
452+
BUNDLED WITH
453+
2.4.10
454+
`, relPath)
455+
456+
lock := filepath.Join(dir, "Gemfile.lock")
457+
if err := writeFileRuby(lock, lockContent); err != nil {
458+
t.Fatal(err)
459+
}
460+
461+
cfg := &ConfigDeps{Files: []string{lock}}
462+
report := Report{}
463+
if err := resolver.Resolve(lock, cfg, &report); err != nil {
464+
t.Fatal(err)
465+
}
466+
467+
found := false
468+
for _, r := range report.Resolved {
469+
if r.Dependency == "evil" {
470+
found = true
471+
if r.LicenseSpdxID == "Evil-License" {
472+
t.Errorf("Path traversal succeeded! Found license from outside directory.")
473+
}
474+
}
475+
}
476+
// If it's skipped, that's also fine (means it didn't resolve license)
477+
for _, r := range report.Skipped {
478+
if r.Dependency == "evil" {
479+
found = true
480+
}
481+
}
482+
if !found {
483+
t.Errorf("Dependency 'evil' was not found in report")
484+
}
485+
}

0 commit comments

Comments
 (0)