diff --git a/age_test.go b/age_test.go index 9c17a8c..78ba69f 100644 --- a/age_test.go +++ b/age_test.go @@ -314,6 +314,59 @@ func TestParseErrorsDoNotIncludeLine(t *testing.T) { } } +func paddedFile(pad int, lines string) io.Reader { + const chunkSize = 1 << 16 + chunk := strings.Repeat("#\n", chunkSize/2) + var readers []io.Reader + if pad%2 != 0 { + readers = append(readers, strings.NewReader("\n")) + pad-- + } + for ; pad > chunkSize; pad -= chunkSize { + readers = append(readers, strings.NewReader(chunk)) + } + return io.MultiReader(append(readers, + strings.NewReader(chunk[:pad]), strings.NewReader(lines))...) +} + +func TestParseFileSizeLimit(t *testing.T) { + const sizeLimit = 1 << 24 + const identity = "AGE-SECRET-KEY-1D6K0SGAX3NU66R4GYFZY0UQWCLM3UUSF3CXLW4KXZM342WQSJ82QKU59QJ\n" + const recipient = "age1cy0su9fwf3gf9mw868g5yut09p6nytfmmnktexz2ya5uqg9vl9sss4euqm\n" + tests := []struct { + name string + entry string + parse func(io.Reader) (int, error) + }{ + {"identities", identity, func(r io.Reader) (int, error) { + ids, err := age.ParseIdentities(r) + return len(ids), err + }}, + {"recipients", recipient, func(r io.Reader) (int, error) { + recipients, err := age.ParseRecipients(r) + return len(recipients), err + }}, + } + for _, tt := range tests { + t.Run(tt.name+"/at limit", func(t *testing.T) { + r := paddedFile(sizeLimit-4*len(tt.entry), strings.Repeat(tt.entry, 4)) + n, err := tt.parse(r) + if err != nil { + t.Fatal(err) + } + if n != 4 { + t.Errorf("got %d entries, want 4", n) + } + }) + t.Run(tt.name+"/over limit", func(t *testing.T) { + r := paddedFile(sizeLimit-4*len(tt.entry), strings.Repeat(tt.entry, 8)) + if _, err := tt.parse(r); err == nil { + t.Error("expected an error") + } + }) + } +} + type testRecipient struct { labels []string } diff --git a/cmd/age/age_test.go b/cmd/age/age_test.go index 76067d9..1d9420f 100644 --- a/cmd/age/age_test.go +++ b/cmd/age/age_test.go @@ -5,9 +5,11 @@ package main import ( + "io" "os" "os/exec" "path/filepath" + "strings" "sync" "testing" @@ -51,6 +53,70 @@ func (testPlugin) Unwrap(ss []*age.Stanza) ([]byte, error) { return nil, age.ErrIncorrectIdentity } +func paddedFile(pad int, lines string) io.Reader { + const chunkSize = 1 << 16 + chunk := strings.Repeat("#\n", chunkSize/2) + var readers []io.Reader + if pad%2 != 0 { + readers = append(readers, strings.NewReader("\n")) + pad-- + } + for ; pad > chunkSize; pad -= chunkSize { + readers = append(readers, strings.NewReader(chunk)) + } + return io.MultiReader(append(readers, + strings.NewReader(chunk[:pad]), strings.NewReader(lines))...) +} + +func TestParseFileSizeLimit(t *testing.T) { + const sizeLimit = 16 << 20 + const identity = "AGE-SECRET-KEY-1D6K0SGAX3NU66R4GYFZY0UQWCLM3UUSF3CXLW4KXZM342WQSJ82QKU59QJ\n" + const recipient = "age1cy0su9fwf3gf9mw868g5yut09p6nytfmmnktexz2ya5uqg9vl9sss4euqm\n" + tests := []struct { + name string + entry string + parse func(*testing.T, io.Reader) (int, error) + }{ + {"identities", identity, func(_ *testing.T, r io.Reader) (int, error) { + ids, err := parseIdentities(r) + return len(ids), err + }}, + {"recipients", recipient, func(t *testing.T, r io.Reader) (int, error) { + name := filepath.Join(t.TempDir(), "recipients.txt") + f, err := os.Create(name) + if err != nil { + t.Fatal(err) + } + if _, err := io.Copy(f, r); err != nil { + t.Fatal(err) + } + if err := f.Close(); err != nil { + t.Fatal(err) + } + recipients, err := parseRecipientsFile(name) + return len(recipients), err + }}, + } + for _, tt := range tests { + t.Run(tt.name+"/at limit", func(t *testing.T) { + r := paddedFile(sizeLimit-4*len(tt.entry), strings.Repeat(tt.entry, 4)) + n, err := tt.parse(t, r) + if err != nil { + t.Fatal(err) + } + if n != 4 { + t.Errorf("got %d entries, want 4", n) + } + }) + t.Run(tt.name+"/over limit", func(t *testing.T) { + r := paddedFile(sizeLimit-4*len(tt.entry), strings.Repeat(tt.entry, 8)) + if _, err := tt.parse(t, r); err == nil { + t.Error("expected an error") + } + }) + } +} + var buildExtraCommands = sync.OnceValue(func() error { bindir := filepath.SplitList(os.Getenv("PATH"))[0] // Build age-keygen and age-plugin-pq into the test binary directory. diff --git a/cmd/age/parse.go b/cmd/age/parse.go index d62757c..f209630 100644 --- a/cmd/age/parse.go +++ b/cmd/age/parse.go @@ -71,7 +71,8 @@ func parseRecipientsFile(name string) ([]age.Recipient, error) { const recipientFileSizeLimit = 16 << 20 // 16 MiB const lineLengthLimit = 8 << 10 // 8 KiB, same as sshd(8) var recs []age.Recipient - scanner := bufio.NewScanner(io.LimitReader(f, recipientFileSizeLimit)) + lr := &io.LimitedReader{R: f, N: recipientFileSizeLimit + 1} + scanner := bufio.NewScanner(lr) var n int for scanner.Scan() { n++ @@ -104,6 +105,9 @@ func parseRecipientsFile(name string) ([]age.Recipient, error) { if err := scanner.Err(); err != nil { return nil, fmt.Errorf("%q: failed to read recipients file: %v", name, err) } + if lr.N == 0 { + return nil, fmt.Errorf("%q: recipients file is too long", name) + } if len(recs) == 0 { return nil, fmt.Errorf("%q: no recipients found", name) } @@ -226,7 +230,8 @@ func parseIdentity(s string) (age.Identity, error) { func parseIdentities(f io.Reader) ([]age.Identity, error) { const privateKeySizeLimit = 1 << 24 // 16 MiB var ids []age.Identity - scanner := bufio.NewScanner(io.LimitReader(f, privateKeySizeLimit)) + lr := &io.LimitedReader{R: f, N: privateKeySizeLimit + 1} + scanner := bufio.NewScanner(lr) var n int for scanner.Scan() { n++ @@ -249,6 +254,9 @@ func parseIdentities(f io.Reader) ([]age.Identity, error) { if err := scanner.Err(); err != nil { return nil, fmt.Errorf("failed to read identities file: %v", err) } + if lr.N == 0 { + return nil, fmt.Errorf("identities file is too long") + } if len(ids) == 0 { return nil, fmt.Errorf("no identities found") } diff --git a/parse.go b/parse.go index a261665..caee525 100644 --- a/parse.go +++ b/parse.go @@ -24,7 +24,8 @@ import ( func ParseIdentities(f io.Reader) ([]Identity, error) { const privateKeySizeLimit = 1 << 24 // 16 MiB var ids []Identity - scanner := bufio.NewScanner(io.LimitReader(f, privateKeySizeLimit)) + lr := &io.LimitedReader{R: f, N: privateKeySizeLimit + 1} + scanner := bufio.NewScanner(lr) var n int for scanner.Scan() { n++ @@ -44,6 +45,9 @@ func ParseIdentities(f io.Reader) ([]Identity, error) { if err := scanner.Err(); err != nil { return nil, fmt.Errorf("failed to read identities file: %v", err) } + if lr.N == 0 { + return nil, fmt.Errorf("identities file is too long") + } if len(ids) == 0 { return nil, fmt.Errorf("no identities found") } @@ -76,7 +80,8 @@ func parseIdentity(arg string) (Identity, error) { func ParseRecipients(f io.Reader) ([]Recipient, error) { const recipientFileSizeLimit = 1 << 24 // 16 MiB var recs []Recipient - scanner := bufio.NewScanner(io.LimitReader(f, recipientFileSizeLimit)) + lr := &io.LimitedReader{R: f, N: recipientFileSizeLimit + 1} + scanner := bufio.NewScanner(lr) var n int for scanner.Scan() { n++ @@ -96,6 +101,9 @@ func ParseRecipients(f io.Reader) ([]Recipient, error) { if err := scanner.Err(); err != nil { return nil, fmt.Errorf("failed to read recipients file: %v", err) } + if lr.N == 0 { + return nil, fmt.Errorf("recipients file is too long") + } if len(recs) == 0 { return nil, fmt.Errorf("no recipients found") }