From ff5ad3fd5a6bd420c396c062cc4af5c65d5eec05 Mon Sep 17 00:00:00 2001 From: Patryk Kalinowski Date: Fri, 19 Jun 2026 17:54:10 +0200 Subject: [PATCH 1/2] fix: validate AES-CBC ciphertext block alignment in Decrypt Add block-size validation after IV removal to prevent panic from cipher.cbcDecrypter.CryptBlocks on malformed (non-block-aligned) ciphertext. Returns a descriptive error instead. Co-Authored-By: Claude Opus 4.6 (1M context) --- aescbc/aescbc.go | 5 +++++ aescbc/aescbc_test.go | 33 +++++++++++++++++++++++++++++++++ 2 files changed, 38 insertions(+) diff --git a/aescbc/aescbc.go b/aescbc/aescbc.go index 89bc153..25c0f7d 100644 --- a/aescbc/aescbc.go +++ b/aescbc/aescbc.go @@ -28,6 +28,11 @@ func Decrypt(key []byte, ciphertext []byte) ([]byte, error) { iv := ciphertext[:aes.BlockSize] ciphertext = ciphertext[aes.BlockSize:] + + if len(ciphertext) == 0 || len(ciphertext)%aes.BlockSize != 0 { + return nil, fmt.Errorf("ciphertext after IV must be a non-zero multiple of %d bytes but was %d", aes.BlockSize, len(ciphertext)) + } + decrypter := cipher.NewCBCDecrypter(block, iv) plaintextData := make([]byte, len(ciphertext)) diff --git a/aescbc/aescbc_test.go b/aescbc/aescbc_test.go index 4598afe..aa6f068 100644 --- a/aescbc/aescbc_test.go +++ b/aescbc/aescbc_test.go @@ -30,6 +30,39 @@ func TestDecrypt(t *testing.T) { assert.Equal(t, []byte("Hello world"), dec) } +func TestDecryptBlockSizeValidation(t *testing.T) { + key := []byte("12345678901234567890123456789012") // 32 bytes + + t.Run("IV only, no data", func(t *testing.T) { + ciphertext := make([]byte, 16) // exactly aes.BlockSize + _, err := aescbc.Decrypt(key, ciphertext) + assert.ErrorContains(t, err, "ciphertext after IV must be a non-zero multiple of 16 bytes but was 0") + }) + + t.Run("non-block-aligned after IV", func(t *testing.T) { + ciphertext := make([]byte, 17) // aes.BlockSize + 1 + _, err := aescbc.Decrypt(key, ciphertext) + assert.ErrorContains(t, err, "ciphertext after IV must be a non-zero multiple of 16 bytes but was 1") + }) + + t.Run("block-aligned after IV does not panic", func(t *testing.T) { + ciphertext := make([]byte, 32) // aes.BlockSize + aes.BlockSize + assert.NotPanics(t, func() { + // Will fail on padding validation, but must not panic + _, _ = aescbc.Decrypt(key, ciphertext) + }) + }) + + t.Run("round-trip", func(t *testing.T) { + plaintext := []byte("security audit fix") + enc, err := aescbc.Encrypt(rand.Reader, key, plaintext) + require.NoError(t, err) + dec, err := aescbc.Decrypt(key, enc) + require.NoError(t, err) + assert.Equal(t, plaintext, dec) + }) +} + func FuzzEncryptAndDecrypt(f *testing.F) { f.Add(uint64(0), uint64(0), uint64(0), uint64(0), []byte("hello")) f.Fuzz(func(t *testing.T, u0, u1, u2, u3 uint64, plaintext []byte) { From 713485300e3e7b343d84945df98c2f4493b875a4 Mon Sep 17 00:00:00 2001 From: Patryk Kalinowski Date: Fri, 19 Jun 2026 17:57:20 +0200 Subject: [PATCH 2/2] ci: update Go version to 1.26.x to match go.mod go.mod requires go >= 1.26.1, but CI workflows were pinned to 1.25.x, causing all jobs to fail. Co-Authored-By: Claude Opus 4.6 (1M context) --- .github/workflows/lint.yml | 2 +- .github/workflows/test.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index be92b64..13df020 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -11,7 +11,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - go-version: ["1.25.x"] + go-version: ["1.26.x"] steps: - uses: actions/checkout@v5 diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 41b3e54..49ee8d7 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -10,7 +10,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - go-version: ["1.25.x"] + go-version: ["1.26.x"] steps: - uses: actions/checkout@v5