Skip to content

Commit e9b65df

Browse files
committed
plumbing: format/index, Improve v4 entry name validation
Signed-off-by: Paulo Gomes <[email protected]>
1 parent adad18d commit e9b65df

2 files changed

Lines changed: 125 additions & 7 deletions

File tree

plumbing/format/index/decoder.go

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,8 @@ import (
44
"bufio"
55
"bytes"
66
"errors"
7+
"fmt"
78
"io"
8-
99
"strconv"
1010
"time"
1111

@@ -26,12 +26,14 @@ var (
2626
ErrInvalidChecksum = errors.New("invalid checksum")
2727
// ErrUnknownExtension is returned when an index extension is encountered that is considered mandatory
2828
ErrUnknownExtension = errors.New("unknown extension")
29+
// ErrMalformedIndexFile is returned when the index file contents are
30+
// structurally invalid.
31+
ErrMalformedIndexFile = errors.New("index decoder: malformed index file")
2932
)
3033

3134
const (
3235
entryHeaderLength = 62
3336
entryExtended = 0x4000
34-
entryValid = 0x8000
3537
nameMask = 0xfff
3638
intentToAddMask = 1 << 13
3739
skipWorkTreeMask = 1 << 14
@@ -177,7 +179,14 @@ func (d *Decoder) doReadEntryNameV4() (string, error) {
177179

178180
var base string
179181
if d.lastEntry != nil {
182+
if l < 0 || int(l) > len(d.lastEntry.Name) {
183+
return "", fmt.Errorf("%w: invalid V4 entry name strip length %d (previous name length: %d)",
184+
ErrMalformedIndexFile, l, len(d.lastEntry.Name))
185+
}
180186
base = d.lastEntry.Name[:len(d.lastEntry.Name)-int(l)]
187+
} else if l > 0 {
188+
return "", fmt.Errorf("%w: non-zero strip length %d on first V4 entry",
189+
ErrMalformedIndexFile, l)
181190
}
182191

183192
name, err := binary.ReadUntil(d.r, '\x00')
@@ -312,7 +321,7 @@ func (d *Decoder) readChecksum(expected []byte) error {
312321
}
313322

314323
func validateHeader(r io.Reader) (version uint32, err error) {
315-
var s = make([]byte, 4)
324+
s := make([]byte, 4)
316325
if _, err := io.ReadFull(r, s); err != nil {
317326
return 0, err
318327
}

plumbing/format/index/decoder_test.go

Lines changed: 113 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,14 @@ package index
33
import (
44
"bytes"
55
"crypto"
6-
"github.com/go-git/go-git/v5/plumbing/hash"
7-
"github.com/go-git/go-git/v5/utils/binary"
86
"io"
97
"testing"
8+
"time"
9+
10+
"github.com/go-git/go-git/v5/plumbing/hash"
11+
"github.com/go-git/go-git/v5/utils/binary"
12+
"github.com/stretchr/testify/assert"
13+
"github.com/stretchr/testify/require"
1014

1115
"github.com/go-git/go-git/v5/plumbing"
1216
"github.com/go-git/go-git/v5/plumbing/filemode"
@@ -87,7 +91,6 @@ func (s *IndexSuite) TestDecodeCacheTree(c *C) {
8791
c.Assert(idx.Cache.Entries[i].Trees, Equals, expected.Trees)
8892
c.Assert(idx.Cache.Entries[i].Hash.String(), Equals, expected.Hash.String())
8993
}
90-
9194
}
9295

9396
var expectedEntries = []TreeEntry{
@@ -133,7 +136,6 @@ func (s *IndexSuite) TestDecodeMergeConflict(c *C) {
133136
c.Assert(e.Hash.String(), Equals, expected[i].Hash)
134137
c.Assert(e.Name, Equals, "go/example.go")
135138
}
136-
137139
}
138140

139141
func (s *IndexSuite) TestDecodeExtendedV3(c *C) {
@@ -320,3 +322,110 @@ func (s *IndexSuite) TestDecodeInvalidHash(c *C) {
320322
err = d.Decode(idx)
321323
c.Assert(err, ErrorMatches, ErrInvalidChecksum.Error())
322324
}
325+
326+
func TestDecodeV4StripLength(t *testing.T) {
327+
t.Parallel()
328+
329+
tests := []struct {
330+
name string
331+
stripLen byte
332+
// firstEntry selects which entry's strip length to corrupt.
333+
// When true the first entry's varint is patched (lastEntry == nil path).
334+
// When false the second entry's varint is patched (lastEntry != nil path).
335+
firstEntry bool
336+
wantErr error
337+
}{
338+
{
339+
name: "SHA1: strip length equals name length",
340+
stripLen: 3, // len("abc") — strips entire name, valid
341+
},
342+
{
343+
name: "SHA1: strip length exceeds name length by one",
344+
stripLen: 4, // len("abc")+1 — one past the end
345+
wantErr: ErrMalformedIndexFile,
346+
},
347+
{
348+
name: "SHA1: strip length far exceeds name length",
349+
stripLen: 100,
350+
wantErr: ErrMalformedIndexFile,
351+
},
352+
{
353+
name: "SHA1: non-zero strip length on first entry",
354+
stripLen: 1,
355+
firstEntry: true,
356+
wantErr: ErrMalformedIndexFile,
357+
},
358+
}
359+
360+
for _, tc := range tests {
361+
t.Run(tc.name, func(t *testing.T) {
362+
t.Parallel()
363+
364+
var h1, h2 plumbing.Hash
365+
idx := &Index{
366+
Version: 4,
367+
Entries: []*Entry{
368+
{
369+
CreatedAt: time.Now(),
370+
ModifiedAt: time.Now(),
371+
Name: "abc",
372+
Hash: h1,
373+
Size: 1,
374+
},
375+
{
376+
CreatedAt: time.Now(),
377+
ModifiedAt: time.Now(),
378+
Name: "abd",
379+
Hash: h2,
380+
Size: 2,
381+
},
382+
},
383+
}
384+
385+
buf := bytes.NewBuffer(nil)
386+
enc := NewEncoder(buf)
387+
err := enc.Encode(idx)
388+
require.NoError(t, err)
389+
390+
raw := buf.Bytes()
391+
392+
hashSize := crypto.SHA1.Size()
393+
h := crypto.SHA1.New()
394+
395+
// In a V4 index, entries are sorted and prefix-compressed. After the
396+
// 12-byte header (DIRC + version + count), entry 1 ("abc") is:
397+
// 40 (10×uint32) + hashSize (hash) + 2 (flags) + 1 (varint 0) + 4 ("abc\x00")
398+
// Entry 2 starts after entry 1. Its fixed header is 40 + hashSize + 2 bytes,
399+
// followed by the strip length varint.
400+
entryFixedLen := 40 + hashSize + 2
401+
entry1StripOffset := 12 + entryFixedLen
402+
entry1Len := entryFixedLen + 1 + 4 // + varint(0) + "abc\x00"
403+
entry2StripOffset := 12 + entry1Len + entryFixedLen
404+
405+
var stripLenOffset int
406+
if tc.firstEntry {
407+
stripLenOffset = entry1StripOffset
408+
} else {
409+
stripLenOffset = entry2StripOffset
410+
}
411+
require.Less(t, stripLenOffset, len(raw)-hashSize, "strip length offset must be within data")
412+
413+
// Variable-width int encoding: a single byte with value < 128 encodes directly.
414+
raw[stripLenOffset] = tc.stripLen
415+
416+
// Recompute the checksum over the modified content.
417+
h.Reset()
418+
h.Write(raw[:len(raw)-hashSize])
419+
copy(raw[len(raw)-hashSize:], h.Sum(nil))
420+
421+
dec := NewDecoder(bytes.NewReader(raw))
422+
out := &Index{}
423+
err = dec.Decode(out)
424+
if tc.wantErr != nil {
425+
assert.ErrorIs(t, err, tc.wantErr)
426+
} else {
427+
assert.NoError(t, err)
428+
}
429+
})
430+
}
431+
}

0 commit comments

Comments
 (0)