diff --git a/github/files.go b/github/files.go index d0f7ecc..6d968a3 100644 --- a/github/files.go +++ b/github/files.go @@ -75,13 +75,26 @@ func escapePath(p string) (string, error) { return strings.Join(encoded, "/"), nil } +// maxFileContentSize is the maximum decoded file size (10 MB) to prevent +// resource exhaustion when decoding base64 content from the API. +const maxFileContentSize = 10 * 1024 * 1024 + // decodeBase64Content decodes base64-encoded content from the GitHub contents API. // GitHub returns base64 content with line breaks for formatting; we strip \r and \n before decoding. +// Returns an error if the decoded content exceeds maxFileContentSize. func decodeBase64Content(encoded string) (string, error) { cleaned := strings.NewReplacer("\n", "", "\r", "").Replace(encoded) + // Check estimated decoded size before allocating. + // Base64 encodes 3 bytes into 4 chars, so decoded ~ len*3/4. + if len(cleaned)*3/4 > maxFileContentSize { + return "", fmt.Errorf("file content too large: estimated %d bytes exceeds limit of %d", len(cleaned)*3/4, maxFileContentSize) + } decoded, err := base64.StdEncoding.DecodeString(cleaned) if err != nil { return "", err } + if len(decoded) > maxFileContentSize { + return "", fmt.Errorf("file content too large: %d bytes exceeds limit of %d", len(decoded), maxFileContentSize) + } return string(decoded), nil } diff --git a/github/files_test.go b/github/files_test.go index 62c5412..8385a07 100644 --- a/github/files_test.go +++ b/github/files_test.go @@ -68,7 +68,7 @@ func TestGetFileContentAtRef_DotSegmentError(t *testing.T) { })) defer srv.Close() - c := NewClient(srv.URL, "token") + c := NewClient("token", srv.URL) _, err := c.GetFileContentAtRef(context.Background(), "owner", "repo", "foo/../bar.go", "main") if err == nil { t.Fatal("expected error for path with dot-segments") @@ -77,3 +77,20 @@ func TestGetFileContentAtRef_DotSegmentError(t *testing.T) { t.Errorf("expected 'invalid file path' error, got: %v", err) } } + +func TestDecodeBase64Content_SizeLimit(t *testing.T) { + t.Parallel() + // Create base64 content that would decode to > maxFileContentSize. + // maxFileContentSize is 10MB. Base64 of 11MB worth of zeros. + // We just need something big enough to trigger the estimated size check. + // 14MB of base64 chars (decodes to ~10.5MB). + huge := strings.Repeat("A", 14*1024*1024) + _, err := decodeBase64Content(huge) + if err == nil { + t.Fatal("expected error for oversized content") + } + if !strings.Contains(err.Error(), "too large") { + t.Errorf("expected 'too large' error, got: %v", err) + } +} + diff --git a/github/pr.go b/github/pr.go index 2aa4c79..6bee50b 100644 --- a/github/pr.go +++ b/github/pr.go @@ -199,7 +199,7 @@ func (c *Client) GetCommitStatuses(ctx context.Context, owner, repo, sha string) // - "success" → "success" // - "failure", "action_required", "timed_out" → "failure" // - "cancelled", "skipped", "neutral" → "success" (non-blocking per GitHub check suite semantics) -// - "stale", "waiting" → "pending" +// - "stale" → "pending" (check run became stale before completing) // - unknown values → "pending" (conservative: treat unrecognized conclusions as incomplete) func mapCheckRunStatus(conclusion *string) string { if conclusion == nil { @@ -213,7 +213,7 @@ func mapCheckRunStatus(conclusion *string) string { return "failure" case "cancelled", "skipped", "neutral": return "success" // non-blocking: these do not indicate a blocking failure per GitHub check suite semantics - case "stale", "waiting": + case "stale": return "pending" default: return "pending"