[ruby/openssl] cipher: fix use-after-free in in-place Cipher#update

Take RSTRING_PTR() of the input buffer after resizing the output
buffer, as the input and output buffers may be the same String object.

Also explicitly reject in-place Cipher#update when the input is too
large to process in a single EVP_CipherUpdate() call. We should not
implicitly split the input across multiple calls for in-place
operation because this appears to be unreliable.

Reported by TheSecguy <thesecguy45@gmail.com>.

https://github.com/ruby/openssl/commit/8df76d73ba
This commit is contained in:
Kazuki Yamaguchi 2026-11-07 03:01:13 +09:00 committed by git
parent 11589a5b92
commit 9fc86fc5f9
2 changed files with 18 additions and 9 deletions

View File

@ -325,6 +325,9 @@ ossl_cipher_update_long(EVP_CIPHER_CTX *ctx, unsigned char *out,
int limit = INT_MAX / 2 + 1;
rb_len_t out_len = 0;
if (out == in && in_len > limit)
ossl_raise(eCipherError, "input too large for in-place update");
do {
int in_part_len = in_len > limit ? limit : (int)in_len;
@ -361,19 +364,19 @@ static VALUE
ossl_cipher_update(int argc, VALUE *argv, VALUE self)
{
EVP_CIPHER_CTX *ctx;
unsigned char *in;
rb_len_t in_len, out_len;
VALUE data, str;
rb_scan_args(argc, argv, "11", &data, &str);
GetCipher(self, ctx);
if (!RTEST(rb_attr_get(self, id_key_set)))
ossl_raise(eCipherError, "key not set");
StringValue(data);
in = (unsigned char *)RSTRING_PTR(data);
if (!NIL_P(str))
StringValue(str);
in_len = RSTRING_LEN(data);
GetCipher(self, ctx);
/*
* As of OpenSSL 3.2, there is no reliable way to determine the required
@ -396,15 +399,15 @@ ossl_cipher_update(int argc, VALUE *argv, VALUE self)
if (NIL_P(str))
str = rb_str_buf_new(out_len);
else {
StringValue(str);
if ((rb_len_t)rb_str_capacity(str) >= out_len)
rb_str_modify(str);
else
rb_str_modify_expand(str, out_len - RSTRING_LEN(str));
}
if (!ossl_cipher_update_long(ctx, (unsigned char *)RSTRING_PTR(str),
&out_len, in, in_len))
if (!ossl_cipher_update_long(ctx,
(unsigned char *)RSTRING_PTR(str), &out_len,
(unsigned char *)RSTRING_PTR(data), in_len))
ossl_raise(eCipherError, "EVP_CipherUpdate");
rb_str_set_len(str, out_len);

View File

@ -117,7 +117,7 @@ class OpenSSL::TestCipher < OpenSSL::TestCase
}
end
def test_ctr_if_exists
def test_aes_ctr
# NIST SP 800-38A F.5.1
key = ["2b7e151628aed2a6abf7158809cf4f3c"].pack("H*")
iv = ["f0f1f2f3f4f5f6f7f8f9fafbfcfdfeff"].pack("H*")
@ -125,10 +125,16 @@ class OpenSSL::TestCipher < OpenSSL::TestCase
"ae2d8a571e03ac9c9eb76fac45af8e51"].pack("H*")
ct = ["874d6191b620e3261bef6864990db6ce" \
"9806f66b7970fdff8617187bb9fffdff"].pack("H*")
cipher = new_encryptor("aes-128-ctr", key: key, iv: iv, padding: 0)
cipher = new_encryptor("aes-128-ctr", key: key, iv: iv)
assert_equal ct, cipher.update(pt) << cipher.final
cipher = new_decryptor("aes-128-ctr", key: key, iv: iv, padding: 0)
cipher = new_decryptor("aes-128-ctr", key: key, iv: iv)
assert_equal pt, cipher.update(ct) << cipher.final
# in-place update
cipher = new_encryptor("aes-128-ctr", key: key, iv: iv)
buf = Marshal.load(Marshal.dump(pt))
assert_equal(ct, cipher.update(buf, buf))
assert_equal(ct, buf)
end
def test_update_with_buffer