From 30541a0beb0d6215e2213b37d13ecbfc983a56ac Mon Sep 17 00:00:00 2001 From: Yiftach Cohen Date: Tue, 31 Mar 2026 17:24:06 +0200 Subject: [PATCH 1/4] [PPSC-602] fix: add .armisignore size limit and upgrade go-git (CWE-770, CVE-2026-34165) - Add 1MB size check before reading .armisignore files to prevent memory exhaustion from maliciously large ignore files - Upgrade go-git/go-git/v5 from v5.17.0 to v5.17.2 to address CVE-2026-34165 - Add test for oversized .armisignore rejection --- go.mod | 2 +- go.sum | 4 ++-- internal/scan/repo/ignore.go | 7 +++++++ internal/scan/repo/ignore_test.go | 23 +++++++++++++++++++++++ 4 files changed, 33 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index ffca7e7..6e527c0 100644 --- a/go.mod +++ b/go.mod @@ -7,7 +7,7 @@ require ( github.com/cenkalti/backoff/v4 v4.3.0 github.com/charmbracelet/lipgloss v1.1.0 github.com/distribution/reference v0.6.0 - github.com/go-git/go-git/v5 v5.17.0 + github.com/go-git/go-git/v5 v5.17.2 github.com/mattn/go-runewidth v0.0.21 github.com/muesli/termenv v0.16.0 github.com/schollz/progressbar/v3 v3.19.0 diff --git a/go.sum b/go.sum index abf06dd..d60c4ff 100644 --- a/go.sum +++ b/go.sum @@ -36,8 +36,8 @@ github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 h1:+zs/tPmkDkHx3U66D github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376/go.mod h1:an3vInlBmSxCcxctByoQdvwPiA7DTK7jaaFDBTtu0ic= github.com/go-git/go-billy/v5 v5.8.0 h1:I8hjc3LbBlXTtVuFNJuwYuMiHvQJDq1AT6u4DwDzZG0= github.com/go-git/go-billy/v5 v5.8.0/go.mod h1:RpvI/rw4Vr5QA+Z60c6d6LXH0rYJo0uD5SqfmrrheCY= -github.com/go-git/go-git/v5 v5.17.0 h1:AbyI4xf+7DsjINHMu35quAh4wJygKBKBuXVjV/pxesM= -github.com/go-git/go-git/v5 v5.17.0/go.mod h1:f82C4YiLx+Lhi8eHxltLeGC5uBTXSFa6PC5WW9o4SjI= +github.com/go-git/go-git/v5 v5.17.2 h1:B+nkdlxdYrvyFK4GPXVU8w1U+YkbsgciIR7f2sZJ104= +github.com/go-git/go-git/v5 v5.17.2/go.mod h1:pW/VmeqkanRFqR6AljLcs7EA7FbZaN5MQqO7oZADXpo= github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM= github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= diff --git a/internal/scan/repo/ignore.go b/internal/scan/repo/ignore.go index 03ddd38..d273266 100644 --- a/internal/scan/repo/ignore.go +++ b/internal/scan/repo/ignore.go @@ -1,6 +1,7 @@ package repo import ( + "fmt" "os" "path/filepath" "strings" @@ -8,6 +9,9 @@ import ( "github.com/go-git/go-git/v5/plumbing/format/gitignore" ) +// maxIgnoreFileSize is the maximum allowed size for a .armisignore file (1 MB). +const maxIgnoreFileSize = 1 << 20 + // IgnoreMatcher matches files against ignore patterns. type IgnoreMatcher struct { patterns []gitignore.Pattern @@ -28,6 +32,9 @@ func LoadIgnorePatterns(repoRoot string) (*IgnoreMatcher, error) { } if !info.IsDir() && info.Name() == ".armisignore" { + if info.Size() > maxIgnoreFileSize { + return fmt.Errorf(".armisignore file too large (%d bytes, max %d): %s", info.Size(), maxIgnoreFileSize, path) + } patterns, err := loadIgnoreFile(path, repoRoot) if err != nil { return err diff --git a/internal/scan/repo/ignore_test.go b/internal/scan/repo/ignore_test.go index f7ac0fd..e154252 100644 --- a/internal/scan/repo/ignore_test.go +++ b/internal/scan/repo/ignore_test.go @@ -3,6 +3,7 @@ package repo import ( "os" "path/filepath" + "strings" "testing" ) @@ -97,3 +98,25 @@ func TestLoadIgnorePatternsNested(t *testing.T) { t.Error("Expected nested pattern to match") } } + +func TestLoadIgnorePatternsOversizedFile(t *testing.T) { + tmpDir := t.TempDir() + + oversized := make([]byte, maxIgnoreFileSize+1) + for i := range oversized { + oversized[i] = 'a' + } + + ignoreFile := filepath.Join(tmpDir, ".armisignore") + if err := os.WriteFile(ignoreFile, oversized, 0600); err != nil { + t.Fatalf("Failed to create oversized ignore file: %v", err) + } + + _, err := LoadIgnorePatterns(tmpDir) + if err == nil { + t.Fatal("expected error for oversized .armisignore file") + } + if !strings.Contains(err.Error(), "too large") { + t.Fatalf("expected 'too large' error, got: %v", err) + } +} From fb9d6467afeb49b7171890e6aca872909d8a9818 Mon Sep 17 00:00:00 2001 From: Yiftach Cohen Date: Tue, 31 Mar 2026 17:37:43 +0200 Subject: [PATCH 2/4] [PPSC-602] fix: enforce .armisignore size limit at read time Address Copilot review: the Walk-based info.Size() check was bypassable via symlinks (Lstat returns symlink size, not target) and had a TOCTOU window. Replace with: - Reject symlinked .armisignore files in Walk callback - Use io.LimitReader in loadIgnoreFile to enforce the 1MB cap on actual bytes read, eliminating both the symlink and TOCTOU bypass vectors --- internal/scan/repo/ignore.go | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/internal/scan/repo/ignore.go b/internal/scan/repo/ignore.go index d273266..091eaa6 100644 --- a/internal/scan/repo/ignore.go +++ b/internal/scan/repo/ignore.go @@ -2,6 +2,7 @@ package repo import ( "fmt" + "io" "os" "path/filepath" "strings" @@ -32,8 +33,8 @@ func LoadIgnorePatterns(repoRoot string) (*IgnoreMatcher, error) { } if !info.IsDir() && info.Name() == ".armisignore" { - if info.Size() > maxIgnoreFileSize { - return fmt.Errorf(".armisignore file too large (%d bytes, max %d): %s", info.Size(), maxIgnoreFileSize, path) + if info.Mode()&os.ModeSymlink != 0 { + return fmt.Errorf(".armisignore is a symlink (rejected): %s", path) } patterns, err := loadIgnoreFile(path, repoRoot) if err != nil { @@ -57,10 +58,21 @@ func LoadIgnorePatterns(repoRoot string) (*IgnoreMatcher, error) { } func loadIgnoreFile(ignoreFilePath, repoRoot string) ([]gitignore.Pattern, error) { - data, err := os.ReadFile(ignoreFilePath) // #nosec G304 - ignore file path is constructed internally + f, err := os.Open(ignoreFilePath) // #nosec G304 - ignore file path is constructed internally if err != nil { return nil, err } + defer f.Close() //nolint:errcheck // read-only file + + // Read up to maxIgnoreFileSize+1 to detect files exceeding the limit. + limited := io.LimitReader(f, maxIgnoreFileSize+1) + data, err := io.ReadAll(limited) + if err != nil { + return nil, err + } + if len(data) > maxIgnoreFileSize { + return nil, fmt.Errorf(".armisignore file too large (max %d bytes): %s", maxIgnoreFileSize, ignoreFilePath) + } ignoreDir := filepath.Dir(ignoreFilePath) relDir, err := filepath.Rel(repoRoot, ignoreDir) From 6eba49ada2086ee7f1d01990a132a32e316cddcb Mon Sep 17 00:00:00 2001 From: Yiftach Cohen Date: Tue, 31 Mar 2026 17:43:42 +0200 Subject: [PATCH 3/4] [PPSC-602] fix(ci): add retries to coverage comment GitHub API call The Coverage Comment job failed with a transient GitHub API 503. The github-script action defaults to 0 retries, so any transient server error causes an immediate failure. Adding retries: 3 makes the step resilient to temporary GitHub API outages. --- .github/workflows/ci.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 5bd59e4..86115aa 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -86,6 +86,7 @@ jobs: - name: Post PR comment uses: actions/github-script@v8 with: + retries: 3 script: | const fs = require('fs'); const summary = fs.readFileSync('coverage/coverage.txt', 'utf8'); From adcf45fb6d4187f34c3bf6fccefe142bf09df019 Mon Sep 17 00:00:00 2001 From: Yiftach Cohen Date: Tue, 31 Mar 2026 17:48:27 +0200 Subject: [PATCH 4/4] [PPSC-602] test: add symlink rejection test for .armisignore Cover the symlink rejection path in LoadIgnorePatterns with a test that creates a symlinked .armisignore and asserts the expected error. Skips gracefully on platforms where symlinks aren't supported. --- internal/scan/repo/ignore_test.go | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/internal/scan/repo/ignore_test.go b/internal/scan/repo/ignore_test.go index e154252..298f630 100644 --- a/internal/scan/repo/ignore_test.go +++ b/internal/scan/repo/ignore_test.go @@ -99,6 +99,30 @@ func TestLoadIgnorePatternsNested(t *testing.T) { } } +func TestLoadIgnorePatternsSymlinkRejected(t *testing.T) { + tmpDir := t.TempDir() + + // Create a real file that the symlink will point to. + realFile := filepath.Join(tmpDir, "real-ignore") + if err := os.WriteFile(realFile, []byte("*.log\n"), 0600); err != nil { + t.Fatalf("Failed to create real ignore file: %v", err) + } + + // Create a symlink named .armisignore pointing to the real file. + symlinkPath := filepath.Join(tmpDir, ".armisignore") + if err := os.Symlink(realFile, symlinkPath); err != nil { + t.Skipf("Symlink creation not supported: %v", err) + } + + _, err := LoadIgnorePatterns(tmpDir) + if err == nil { + t.Fatal("expected error for symlinked .armisignore file") + } + if !strings.Contains(err.Error(), "symlink") { + t.Fatalf("expected 'symlink' error, got: %v", err) + } +} + func TestLoadIgnorePatternsOversizedFile(t *testing.T) { tmpDir := t.TempDir()