From 9ea4dcb83e3a94146a13df8dae872973354fa3ee Mon Sep 17 00:00:00 2001 From: Chris Wright Date: Tue, 1 Oct 2013 20:07:48 +0100 Subject: [PATCH 1/3] Implement client-initiated renegotiation rate limiting --- ext/openssl/openssl.c | 6 +++ ext/openssl/xp_ssl.c | 115 +++++++++++++++++++++++++++++++++++++++++- 2 files changed, 120 insertions(+), 1 deletion(-) diff --git a/ext/openssl/openssl.c b/ext/openssl/openssl.c index 90329d06caa6..6a597c8388ed 100644 --- a/ext/openssl/openssl.c +++ b/ext/openssl/openssl.c @@ -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]; diff --git a/ext/openssl/xp_ssl.c b/ext/openssl/xp_ssl.c index 1ac8a0220e3a..e710c79df4e5 100644 --- a/ext/openssl/xp_ssl.c +++ b/ext/openssl/xp_ssl.c @@ -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 @@ -58,6 +59,8 @@ typedef struct _php_openssl_netstream_data_t { char *sni; unsigned state_set:1; unsigned _spare:31; + struct timeval *handshake_history; + int handshake_status; } php_openssl_netstream_data_t; php_stream_ops php_openssl_socket_ops; @@ -215,7 +218,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)); @@ -288,6 +297,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; @@ -303,6 +315,105 @@ 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) +{ + zval **val = NULL; + long limit = 0; + zend_bool limithit = 0; + php_stream *stream; + php_openssl_netstream_data_t *sslsock; + + TSRMLS_FETCH(); + + stream = php_openssl_get_stream_from_ssl_handle(ssl); + sslsock = (php_openssl_netstream_data_t*)stream->abstract; + + if (!(where & SSL_CB_HANDSHAKE_START) || sslsock->is_client) { + return; + } + + if (stream->context && SUCCESS == php_stream_context_get_option(stream->context, "ssl", "handshake_limit_rate", &val)) { + limit = Z_LVAL_PP(val); + } + + if (limit < 0) { + return; + } + + if (limit == 0) { + if (sslsock->handshake_status == 0) { + sslsock->handshake_status = 1; + } else { + limithit = 1; + } + } else if (limit > 0) { + struct timeval now; + int i; + zend_bool carry = 0; + + if (sslsock->handshake_history == NULL) { + sslsock->handshake_history = (struct timeval*)pemalloc( + (size_t)(limit * sizeof(struct timeval)), + php_stream_is_persistent(stream) + ); + sslsock->handshake_status = 0; + } + + gettimeofday(&now, NULL); + for (i = sslsock->handshake_status - 1; i >= 0; i--) { + if (!carry) { + 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 < limit - 1) { + carry = 1; + } else { + limithit = 1; + break; + } + } + + if (carry) { + sslsock->handshake_history[i + 1] = sslsock->handshake_history[i]; + } + } + + if (!limithit) { + sslsock->handshake_history[0] = now; + sslsock->handshake_status += 1; + } + } + + if (limithit) { + zend_bool destroyconnection = 1; + + if (stream->context && SUCCESS == php_stream_context_get_option(stream->context, "ssl", "handshake_limit_callback", &val)) { + zval *param, **params[1], *retval; + + MAKE_STD_ZVAL(param); + php_stream_to_zval(stream, param); + params[0] = ¶m; + + if (FAILURE == call_user_function_ex(EG(function_table), NULL, *val, &retval, 1, params, 0, NULL TSRMLS_CC)) { + php_error_docref(NULL TSRMLS_CC, E_WARNING, "failed to call SSL handshake limit notifier"); + } + + if (Z_TYPE_P(retval) == IS_BOOL && Z_BVAL_P(retval) == 0) { + destroyconnection = 0; + } + + FREE_ZVAL(param); + zval_ptr_dtor(&retval); + } else { + php_error_docref(NULL TSRMLS_CC, E_WARNING, "SSL: client-initiated handshake rate limit exceeded by peer"); + } + + if (destroyconnection) { + sslsock->handshake_status = -1; + } + } +} +/* }}} */ static inline int php_openssl_setup_crypto(php_stream *stream, php_openssl_netstream_data_t *sslsock, @@ -415,6 +526,8 @@ static inline int php_openssl_setup_crypto(php_stream *stream, return -1; } + 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); } From dc10e7a15a8160838fee675167990162e6457d85 Mon Sep 17 00:00:00 2001 From: Daniel Lowrey Date: Tue, 8 Oct 2013 22:25:51 -0400 Subject: [PATCH 2/3] Handshake rate limiting changes -- more after the jump - Removed potential segfault on rate limit increase post stream creation - Stream meta data updated when client closed for reneg policy violation - Added test --- ext/openssl/tests/handshake_limiting.phpt | 88 +++++++++++++++++++ ext/openssl/xp_ssl.c | 101 ++++++++-------------- 2 files changed, 125 insertions(+), 64 deletions(-) create mode 100644 ext/openssl/tests/handshake_limiting.phpt diff --git a/ext/openssl/tests/handshake_limiting.phpt b/ext/openssl/tests/handshake_limiting.phpt new file mode 100644 index 000000000000..3571e73ea463 --- /dev/null +++ b/ext/openssl/tests/handshake_limiting.phpt @@ -0,0 +1,88 @@ +--TEST-- +Encrypted server rate-limits client-initiated TLS renegotiation +--SKIPIF-- + 0) die("skip couldn't locate openssl binary"); +--FILE-- + [ + '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) diff --git a/ext/openssl/xp_ssl.c b/ext/openssl/xp_ssl.c index e710c79df4e5..1475939fe6ee 100644 --- a/ext/openssl/xp_ssl.c +++ b/ext/openssl/xp_ssl.c @@ -61,6 +61,7 @@ typedef struct _php_openssl_netstream_data_t { 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; @@ -317,9 +318,7 @@ static int php_openssl_sockop_stat(php_stream *stream, php_stream_statbuf *ssb T static void php_openssl_info_callback(const SSL *ssl, int where, int ret) { - zval **val = NULL; - long limit = 0; - zend_bool limithit = 0; + zend_bool is_limit_reached = 0; php_stream *stream; php_openssl_netstream_data_t *sslsock; @@ -330,47 +329,27 @@ static void php_openssl_info_callback(const SSL *ssl, int where, int ret) if (!(where & SSL_CB_HANDSHAKE_START) || sslsock->is_client) { return; - } - - if (stream->context && SUCCESS == php_stream_context_get_option(stream->context, "ssl", "handshake_limit_rate", &val)) { - limit = Z_LVAL_PP(val); - } - - if (limit < 0) { - return; - } - - if (limit == 0) { + } else if (sslsock->handshake_limit == 0) { if (sslsock->handshake_status == 0) { sslsock->handshake_status = 1; } else { - limithit = 1; + is_limit_reached = 1; } - } else if (limit > 0) { - struct timeval now; + } else { int i; + struct timeval now; zend_bool carry = 0; - if (sslsock->handshake_history == NULL) { - sslsock->handshake_history = (struct timeval*)pemalloc( - (size_t)(limit * sizeof(struct timeval)), - php_stream_is_persistent(stream) - ); - sslsock->handshake_status = 0; - } - gettimeofday(&now, NULL); for (i = sslsock->handshake_status - 1; i >= 0; i--) { - if (!carry) { - 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 < limit - 1) { - carry = 1; - } else { - limithit = 1; - break; - } + 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) { @@ -378,39 +357,21 @@ static void php_openssl_info_callback(const SSL *ssl, int where, int ret) } } - if (!limithit) { + if (!is_limit_reached) { sslsock->handshake_history[0] = now; sslsock->handshake_status += 1; } } - if (limithit) { - zend_bool destroyconnection = 1; - - if (stream->context && SUCCESS == php_stream_context_get_option(stream->context, "ssl", "handshake_limit_callback", &val)) { - zval *param, **params[1], *retval; - - MAKE_STD_ZVAL(param); - php_stream_to_zval(stream, param); - params[0] = ¶m; - - if (FAILURE == call_user_function_ex(EG(function_table), NULL, *val, &retval, 1, params, 0, NULL TSRMLS_CC)) { - php_error_docref(NULL TSRMLS_CC, E_WARNING, "failed to call SSL handshake limit notifier"); - } - - if (Z_TYPE_P(retval) == IS_BOOL && Z_BVAL_P(retval) == 0) { - destroyconnection = 0; - } - - FREE_ZVAL(param); - zval_ptr_dtor(&retval); - } else { - php_error_docref(NULL TSRMLS_CC, E_WARNING, "SSL: client-initiated handshake rate limit exceeded by peer"); - } - - if (destroyconnection) { - 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; + sslsock->handshake_status = -1; } } /* }}} */ @@ -422,6 +383,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) { @@ -526,7 +488,18 @@ static inline int php_openssl_setup_crypto(php_stream *stream, return -1; } - SSL_CTX_set_info_callback(sslsock->ctx, php_openssl_info_callback); + /* 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); + if (sslsock->handshake_limit >= 0) { + sslsock->handshake_history = (struct timeval*)pemalloc( + (size_t)(sslsock->handshake_limit * sizeof(struct timeval)), + 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); From dc2ed8af9428ab1ed22eaeb960ab8531b3891a1a Mon Sep 17 00:00:00 2001 From: Daniel Lowrey Date: Thu, 10 Oct 2013 09:55:52 -0400 Subject: [PATCH 3/3] Removed unnecessary TSRMLS_FETCH() --- ext/openssl/xp_ssl.c | 2 -- 1 file changed, 2 deletions(-) diff --git a/ext/openssl/xp_ssl.c b/ext/openssl/xp_ssl.c index 1475939fe6ee..ac2ba9d8a07b 100644 --- a/ext/openssl/xp_ssl.c +++ b/ext/openssl/xp_ssl.c @@ -322,8 +322,6 @@ static void php_openssl_info_callback(const SSL *ssl, int where, int ret) php_stream *stream; php_openssl_netstream_data_t *sslsock; - TSRMLS_FETCH(); - stream = php_openssl_get_stream_from_ssl_handle(ssl); sslsock = (php_openssl_netstream_data_t*)stream->abstract;