Skip to content

Client-Initiated Renegotiation DoS Vulnerability #486

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 3 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions ext/openssl/openssl.c
Original file line number Diff line number Diff line change
Expand Up @@ -565,6 +565,12 @@ inline static int php_openssl_open_base_dir_chk(char *filename TSRMLS_DC)
}
/* }}} */

inline php_stream* php_openssl_get_stream_from_ssl_handle(const SSL *ssl)
{
return (php_stream*)SSL_get_ex_data(ssl, ssl_stream_data_index);
}
/* }}} */

/* openssl -> PHP "bridging" */
/* true global; readonly after module startup */
static char default_ssl_conf_filename[MAXPATHLEN];
Expand Down
88 changes: 88 additions & 0 deletions ext/openssl/tests/handshake_limiting.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
--TEST--
Encrypted server rate-limits client-initiated TLS renegotiation
--SKIPIF--
<?php
if (!extension_loaded("openssl")) die("skip");
if (!function_exists('pcntl_fork')) die("skip no fork");
exec('openssl help', $out, $code);
if ($code > 0) die("skip couldn't locate openssl binary");
--FILE--
<?php

/**
* This test uses the openssl binary directly to initiate renegotiation. At this time it's not
* possible renegotiate the TLS handshake in PHP userland, so using the openssl s_client binary
* command is the only feasible way to test renegotiation limiting functionality. It's not an ideal
* solution, but it's really the only way to get test coverage on the rate-limiting functionality
* given current limitations.
*/

$bindTo = 'tcp://127.0.0.1:12345';
$flags = STREAM_SERVER_BIND | STREAM_SERVER_LISTEN;
$server = stream_socket_server($bindTo, $errNo, $errStr, $flags, stream_context_create(['ssl' => [
'local_cert' => dirname(__FILE__) . '/bug54992.pem',
'max_handshake_rate' => 1
]]));

$pid = pcntl_fork();
if ($pid == -1) {
die('could not fork');
} elseif ($pid) {

$cmd = 'openssl s_client -connect localhost:12345';
$descriptorspec = array(
0 => array("pipe", "r"),
1 => array("pipe", "w"),
2 => array("pipe", "w"),
);
$process = proc_open($cmd, $descriptorspec, $pipes);

list($stdin, $stdout, $stderr) = $pipes;

// Trigger renegotiation twice
// Server settings only allow one per second (should result in disconnection)
fwrite($stdin, "R\nR\n");

$lines = [];
while(!feof($stderr)) {
fgets($stderr);
}

fclose($stdin);
fclose($stdout);
fclose($stderr);
proc_close($process);
pcntl_wait($status);

} else {

$clients = [];
while (1) {
$r = array_merge([$server], $clients);
$w = $e = [];
stream_select($r, $w, $e, $timeout=1);

foreach ($r as $sock) {
if ($sock === $server && ($client = @stream_socket_accept($server, $timeout = 1))) {
stream_socket_enable_crypto($client, TRUE, STREAM_CRYPTO_METHOD_TLS_SERVER);
$clientId = (int) $client;
$clients[$clientId] = $client;
} else {
$clientId = (int) $client;
$buffer = @fread($sock, 1024);

if (strlen($buffer)) {
continue;
} elseif (feof($sock)) {
$meta = !empty(stream_get_meta_data($sock)['wrapper_data']['handshake_limit_exceeded']);
var_dump($meta);
fclose($sock);
unset($clients[$clientId]);
break 2;
}
}
}
}
}
--EXPECTF--
bool(true)
86 changes: 85 additions & 1 deletion ext/openssl/xp_ssl.c
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@

int php_openssl_apply_verification_policy(SSL *ssl, X509 *peer, php_stream *stream TSRMLS_DC);
SSL *php_SSL_new_from_context(SSL_CTX *ctx, php_stream *stream TSRMLS_DC);
php_stream* php_openssl_get_stream_from_ssl_handle(const SSL *ssl);
int php_openssl_get_x509_list_id(void);

/* This implementation is very closely tied to the that of the native
Expand All @@ -58,6 +59,9 @@ typedef struct _php_openssl_netstream_data_t {
char *sni;
unsigned state_set:1;
unsigned _spare:31;
struct timeval *handshake_history;
int handshake_status;
int handshake_limit;
} php_openssl_netstream_data_t;

php_stream_ops php_openssl_socket_ops;
Expand Down Expand Up @@ -215,7 +219,13 @@ static size_t php_openssl_sockop_read(php_stream *stream, char *buf, size_t coun
do {
nr_bytes = SSL_read(sslsock->ssl_handle, buf, count);

if (nr_bytes <= 0) {
if (sslsock->handshake_status < 0) {
/* renegotiation rate limiting triggered */
php_stream_xport_shutdown(stream, (stream_shutdown_t)SHUT_RDWR TSRMLS_CC);
nr_bytes = 0;
stream->eof = 1;
break;
} else if (nr_bytes <= 0) {
retry = handle_ssl_error(stream, nr_bytes, 0 TSRMLS_CC);
stream->eof = (retry == 0 && errno != EAGAIN && !SSL_pending(sslsock->ssl_handle));

Expand Down Expand Up @@ -288,6 +298,9 @@ static int php_openssl_sockop_close(php_stream *stream, int close_handle TSRMLS_
if (sslsock->sni) {
pefree(sslsock->sni, php_stream_is_persistent(stream));
}
if (sslsock->handshake_history) {
pefree(sslsock->handshake_history, php_stream_is_persistent(stream));
}
pefree(sslsock, php_stream_is_persistent(stream));

return 0;
Expand All @@ -303,6 +316,63 @@ static int php_openssl_sockop_stat(php_stream *stream, php_stream_statbuf *ssb T
return php_stream_socket_ops.stat(stream, ssb TSRMLS_CC);
}

static void php_openssl_info_callback(const SSL *ssl, int where, int ret)
{
zend_bool is_limit_reached = 0;
php_stream *stream;
php_openssl_netstream_data_t *sslsock;

stream = php_openssl_get_stream_from_ssl_handle(ssl);
sslsock = (php_openssl_netstream_data_t*)stream->abstract;

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Both stream and sslock, never ever could be NULL?

if (!(where & SSL_CB_HANDSHAKE_START) || sslsock->is_client) {
return;
} else if (sslsock->handshake_limit == 0) {
if (sslsock->handshake_status == 0) {
sslsock->handshake_status = 1;
} else {
is_limit_reached = 1;
}
} else {
int i;
struct timeval now;
zend_bool carry = 0;

gettimeofday(&now, NULL);
for (i = sslsock->handshake_status - 1; i >= 0; i--) {
time_t secsago = now.tv_sec - sslsock->handshake_history[i].tv_sec;
if (secsago > 1 || (secsago == 1 && sslsock->handshake_history[i].tv_usec <= now.tv_usec)) {
sslsock->handshake_status -= 1;
} else if (i < sslsock->handshake_limit - 1) {
carry = 1;
} else {
is_limit_reached = 1;
break;
}

if (carry) {
sslsock->handshake_history[i + 1] = sslsock->handshake_history[i];
}
}

if (!is_limit_reached) {
sslsock->handshake_history[0] = now;
sslsock->handshake_status += 1;
}
}

if (is_limit_reached) {
zval *data, *arr;
MAKE_STD_ZVAL(data);
ZVAL_BOOL(data, 1);
MAKE_STD_ZVAL(arr);
array_init(arr);
add_assoc_zval(arr, "handshake_limit_exceeded", data);
stream->wrapperdata = arr;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What if stream->wrapperdata is already set?

sslsock->handshake_status = -1;
}
}
/* }}} */

static inline int php_openssl_setup_crypto(php_stream *stream,
php_openssl_netstream_data_t *sslsock,
Expand All @@ -311,6 +381,7 @@ static inline int php_openssl_setup_crypto(php_stream *stream,
{
const SSL_METHOD *method;
long ssl_ctx_options = SSL_OP_ALL;
zval **handshake_limit_val;

if (sslsock->ssl_handle) {
if (sslsock->s.is_blocked) {
Expand Down Expand Up @@ -415,6 +486,19 @@ static inline int php_openssl_setup_crypto(php_stream *stream,
return -1;
}

/* Enable callback for client renegotiation DoS protection */
if (stream->context && SUCCESS == php_stream_context_get_option(stream->context, "ssl", "max_handshake_rate", &handshake_limit_val )) {
sslsock->handshake_limit = Z_LVAL_PP(handshake_limit_val);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What if Z_TYPE_PP(handshake_limit_val) != IS_LONG?

if (sslsock->handshake_limit >= 0) {
sslsock->handshake_history = (struct timeval*)pemalloc(
(size_t)(sslsock->handshake_limit * sizeof(struct timeval)),
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There's safe_(p)emalloc for that use case.

php_stream_is_persistent(stream)
);
sslsock->handshake_status = 0;
SSL_CTX_set_info_callback(sslsock->ctx, php_openssl_info_callback);
}
}

if (!SSL_set_fd(sslsock->ssl_handle, sslsock->s.socket)) {
handle_ssl_error(stream, 0, 1 TSRMLS_CC);
}
Expand Down