diff --git a/.jules/sentinel.md b/.jules/sentinel.md new file mode 100644 index 000000000..ec2af4f1d --- /dev/null +++ b/.jules/sentinel.md @@ -0,0 +1,4 @@ +## 2026-06-16 - Unhandled IndexError Hardening in Cryptography Routines +**Vulnerability:** Amber's `MessageVerifier` and `MessageEncryptor` attempted to slice arrays and decrypt payloads before checking the bounds of the provided data (`IndexError` due to missing `split` size check and negative offsets in slices). While top-level middleware rescues and returns a 500 status code, these classes should gracefully reject malformed payloads themselves with custom exceptions. +**Learning:** Crystal's array slicing and destructuring raise `IndexError` when bounds are not met, which propagates to an uncaught exception instead of a graceful rejection. Always check payload sizes before executing cryptography functions. +**Prevention:** Always validate size and structure before extracting signatures, IVs, or payload data from untrusted inputs in cryptography routines. Add `size` bounds checks and safely handle destructuring, raising typed exceptions like `InvalidSignature` and `InvalidMessage`. diff --git a/spec/amber/support/message_encryptor_spec.cr b/spec/amber/support/message_encryptor_spec.cr new file mode 100644 index 000000000..7addb153a --- /dev/null +++ b/spec/amber/support/message_encryptor_spec.cr @@ -0,0 +1,31 @@ +require "../../spec_helper" + +module Amber::Support + describe MessageEncryptor do + secret = "a" * 32 + encryptor = MessageEncryptor.new(secret) + + it "encrypts and decrypts valid messages successfully" do + message = "Hello, world!" + encrypted = encryptor.encrypt_and_sign(message) + decrypted = encryptor.verify_and_decrypt(encrypted) + String.new(decrypted).should eq message + end + + it "raises Exceptions::InvalidMessage for undersized payloads in verify_and_decrypt" do + # signature_size is 32, block_size is 16. Total required: >= 48 bytes + undersized = Bytes.new(20) + expect_raises(Exceptions::InvalidMessage, "Invalid message size") do + encryptor.verify_and_decrypt(undersized) + end + end + + it "raises Exceptions::InvalidMessage for undersized payloads in decrypt" do + # block_size is 16. decrypt expects >= 16 bytes + undersized = Bytes.new(10) + expect_raises(Exceptions::InvalidMessage, "Invalid message size") do + encryptor.decrypt(undersized) + end + end + end +end diff --git a/spec/amber/support/message_verifier_spec.cr b/spec/amber/support/message_verifier_spec.cr new file mode 100644 index 000000000..32ba25076 --- /dev/null +++ b/spec/amber/support/message_verifier_spec.cr @@ -0,0 +1,27 @@ +require "../../spec_helper" + +module Amber::Support + describe MessageVerifier do + secret = "a" * 32 + verifier = MessageVerifier.new(secret) + + it "signs and verifies valid messages successfully" do + message = "Hello, world!" + signed = verifier.generate(message) + verified = verifier.verified(signed) + verified.should eq message + end + + it "returns nil for malformed/no-delimiter payloads in verified" do + malformed = "invalidpayload" + verifier.verified(malformed).should be_nil + end + + it "raises Exceptions::InvalidSignature for malformed/no-delimiter payloads in verify_raw" do + malformed = "invalidpayload" + expect_raises(Exceptions::InvalidSignature) do + verifier.verify_raw(malformed) + end + end + end +end diff --git a/src/amber/support/message_encryptor.cr b/src/amber/support/message_encryptor.cr index 8ebbb1133..ed31ebbd7 100644 --- a/src/amber/support/message_encryptor.cr +++ b/src/amber/support/message_encryptor.cr @@ -18,6 +18,10 @@ module Amber::Support # Verify and Decrypt a message. We need to verify the message in order to # avoid padding attacks. Reference: http://www.limited-entropy.com/padding-oracle-attacks. def verify_and_decrypt(value : Bytes) : Bytes + if value.size < @signature_size + @block_size + raise Exceptions::InvalidMessage.new("Invalid message size") + end + signature = value[value.size - @signature_size, @signature_size] data_iv = value[0, value.size - @signature_size] if Crypto::Subtle.constant_time_compare(sign_bytes(data_iv), signature) @@ -48,6 +52,10 @@ module Amber::Support end def decrypt(value : Bytes) + if value.size < @block_size + raise Exceptions::InvalidMessage.new("Invalid message size") + end + cipher = OpenSSL::Cipher.new(@cipher_algorithm) data = value[0, value.size - @block_size] iv = value[value.size - @block_size, @block_size] diff --git a/src/amber/support/message_verifier.cr b/src/amber/support/message_verifier.cr index 0d40f5b12..c78b47f10 100644 --- a/src/amber/support/message_verifier.cr +++ b/src/amber/support/message_verifier.cr @@ -11,7 +11,10 @@ module Amber::Support end def verified(signed_message : String) - data, digest = signed_message.split("--") + parts = signed_message.split("--", 2) + return unless parts.size == 2 + data, digest = parts[0], parts[1] + if valid_message?(data, digest) String.new(decode(data)) end @@ -25,7 +28,10 @@ module Amber::Support end def verify_raw(signed_message : String) : Bytes - data, digest = signed_message.split("--") + parts = signed_message.split("--", 2) + raise(Exceptions::InvalidSignature.new) unless parts.size == 2 + data, digest = parts[0], parts[1] + if valid_message?(data, digest) decode(data) else