Since 2011, a bug has existed in OpenSSL that means innocuous code like:
require('tls').connect({port: 443, NPNProtocols: new Uint8Array()}, function(c) {})
or (equivalently, in Python):
import ssl, socket
assert ssl.HAS_NPN
ctx = ssl.create_default_context()
ctx.set_npn_protocols([])
ctx.load_verify_locations('root.crt')
sock = socket.create_connection(('127.0.0.1', 443))
sock = ctx.wrap_socket(sock, server_hostname='localhost')
sock.write('hello')
Silently sends up to 255 bytes of the client’s heap to the server. The server must support NPN, and the heap data is encrypted in transit.
This is confirmed to affect Python <= 3.9 and Node <= 9. It may equally
affect any program that calls SSL_select_next_proto
with a client
buffer that is not a valid list of protocols (this includes an empty buffer).
Meeting those constraints is quite unlikely nowadays:
However, they were much more common in the lifetime of this bug. You should review your historic usage
of SSL_select_next_proto
and if you could have ever triggered this bug, I would suggest rolling any
secrets available to affected programs.
Android was affected up until 2014, with the heap leak behaviour observed independently by an okhttp developer.
SSL_select_next_proto
, however NPN support was removed in 2017
so is not meaningfully broken.SSL_select_next_proto
memory unsafety while rewriting it in rust.The various audits, code reviews, static analysis, dynamic analysis, fuzzing, and popularity were not sufficient to remedy this bug earlier.
Though valgrind immediately points it out once triggered, and a really basic static pointer bounds analysis should also find it (though likely with a huge false-positive rate in other code.)
The known exposures to this bug (Python, Node, Android) are probably not all of them, but most likely will be historic.
SSL_select_next_proto
.SSL_select_next_proto
called with client_len == 0
unconditionally reads
client[0]
and returns the pointer client + 1
. Thereafter, *out
points
outside a valid address, and *outlen
contains an undefined value.
Minimal reproducer:
#include <openssl/ssl.h>
#include <stdint.h>
#include <stdio.h>
int main() {
uint8_t client_input[] = {};
uint8_t *output = NULL;
uint8_t output_len = 0;
const uint8_t server_input[] = {2, 'h', '2'};
int ret = SSL_select_next_proto(&output, &output_len, server_input,
sizeof(server_input), client_input, 0);
printf("ret = %d\n", ret);
printf("output = %p\n", output);
printf("output_len = %u\n", (unsigned)output_len);
return 0;
}
SSL_select_next_proto
is a normal API function so the parameters passed
to it are ultimately up to the application. However it is almost always used inside
an ALPN or NPN callback:
If SSL_select_next_proto
is used straightforwardly in a server’s ALPN callback,
there is no impact because other code in openssl validates the client’s extension as being
non-empty.
If SSL_select_next_proto
is used straightforwardly in a client’s NPN callback,
the client data and its length is typically a matter of local configuration and
not under openssl’s control. Most importantly, the impact of the wrong length and
pointer being returned from this function is a catastrophic memory safety failure:
the memory is copied, placed into the selected_protocol
field of the
NextProtocolNegotiationEncryptedExtension
and sent to the peer!
As a realistic demonstration, in python3.92 the following code sees OpenSSL send a chunk of heap to the server:
import ssl, socket
assert ssl.HAS_NPN
ctx = ssl.create_default_context()
ctx.set_npn_protocols([])
ctx.load_verify_locations('root.crt')
sock = socket.create_connection(('127.0.0.1', 443))
sock = ctx.wrap_socket(sock, server_hostname='localhost')
sock.write('hello')
The peer receives (for example):
NextProtocolNegotiationEncryptedExtension {
selected_protocol: c2d60ba35f00000000000000000000000000000000000031000000000000000
00000000000000060f3030d5e7f0000e0f5b9f1a65f00002006aaf1a65f0000
d04b0f0d5e7f0000210000000000000000f6b9f1a65f000030f6b9f1a65f000
0d0ebb9f1a65f00003100000000000000,
padding: 000000000000000000000000000000,
}