age,cmd/age: reject oversized key files

Reported by Joe Doyle of Trail of Bits.
This commit is contained in:
Filippo Valsorda
2026-08-29 19:30:10 +02:00
parent c45ccfd215
commit 418c7ac33b
4 changed files with 139 additions and 4 deletions
+66
View File
@@ -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.
+10 -2
View File
@@ -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")
}