diff --git a/ext/openssl/openssl.c b/ext/openssl/openssl.c index 8b77d28ad1e..c813a7ea29c 100755 --- a/ext/openssl/openssl.c +++ b/ext/openssl/openssl.c @@ -585,6 +585,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/php_openssl.h b/ext/openssl/php_openssl.h index cab787afedd..a823d30bd8c 100644 --- a/ext/openssl/php_openssl.h +++ b/ext/openssl/php_openssl.h @@ -29,6 +29,10 @@ extern zend_module_entry openssl_module_entry; #define OPENSSL_RAW_DATA 1 #define OPENSSL_ZERO_PADDING 2 +/* Used for client-initiated handshake renegotiation DoS protection*/ +#define DEFAULT_RENEG_LIMIT 2 +#define DEFAULT_RENEG_WINDOW 300 + php_stream_transport_factory_func php_openssl_ssl_socket_factory; PHP_MINIT_FUNCTION(openssl); diff --git a/ext/openssl/php_openssl_structs.h b/ext/openssl/php_openssl_structs.h index 13f8f320f8c..562c756cd35 100644 --- a/ext/openssl/php_openssl_structs.h +++ b/ext/openssl/php_openssl_structs.h @@ -22,6 +22,14 @@ #include "php_network.h" #include +typedef struct _php_openssl_handshake_bucket_t { + long prev_handshake; + long limit; + long window; + float tokens; + unsigned should_close; +} php_openssl_handshake_bucket_t; + /* This implementation is very closely tied to the that of the native * sockets implemented in the core. * Don't try this technique in other extensions! @@ -36,6 +44,7 @@ typedef struct _php_openssl_netstream_data_t { int is_client; int ssl_active; php_stream_xport_crypt_method_t method; + php_openssl_handshake_bucket_t *reneg; char *url_name; unsigned state_set:1; unsigned _spare:31; diff --git a/ext/openssl/tests/stream_server_reneg_limit.phpt b/ext/openssl/tests/stream_server_reneg_limit.phpt new file mode 100644 index 00000000000..134d3cb601a --- /dev/null +++ b/ext/openssl/tests/stream_server_reneg_limit.phpt @@ -0,0 +1,89 @@ +--TEST-- +TLS server rate-limits client-initiated renegotiation +--SKIPIF-- + 0) die("skip couldn't locate openssl binary"); +--FILE-- + [ + 'local_cert' => __DIR__ . '/bug54992.pem', + 'reneg_limit' => 0, + 'reneg_window' => 30, + 'reneg_limit_callback' => function($stream) { + var_dump($stream); + } +]])); + +$pid = pcntl_fork(); +if ($pid == -1) { + die('could not fork'); +} elseif ($pid) { + + $cmd = 'openssl s_client -connect 127.0.0.1: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\nR\nR\n"); + + $lines = []; + while(!feof($stderr)) { + fgets($stderr); + } + + fclose($stdin); + fclose($stdout); + fclose($stderr); + proc_terminate($process); + pcntl_wait($status); + +} else { + + $clients = []; + + while (1) { + $r = array_merge([$server], $clients); + $w = $e = []; + + stream_select($r, $w, $e, $timeout=42); + + foreach ($r as $sock) { + if ($sock === $server && ($client = stream_socket_accept($server, $timeout = 42))) { + $clientId = (int) $client; + $clients[$clientId] = $client; + } elseif ($sock !== $server) { + $clientId = (int) $sock; + $buffer = fread($sock, 1024); + if (strlen($buffer)) { + continue; + } elseif (!is_resource($sock) || feof($sock)) { + unset($clients[$clientId]); + break 2; + } + } + } + } +} +--EXPECTF-- +resource(%d) of type (stream) diff --git a/ext/openssl/xp_ssl.c b/ext/openssl/xp_ssl.c index 660caa7f65d..c122f481921 100644 --- a/ext/openssl/xp_ssl.c +++ b/ext/openssl/xp_ssl.c @@ -51,6 +51,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); php_stream_ops php_openssl_socket_ops; @@ -208,7 +209,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->reneg && sslsock->reneg->should_close) { + /* 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)); @@ -234,13 +241,13 @@ static size_t php_openssl_sockop_read(php_stream *stream, char *buf, size_t coun return nr_bytes; } - static int php_openssl_sockop_close(php_stream *stream, int close_handle TSRMLS_DC) { php_openssl_netstream_data_t *sslsock = (php_openssl_netstream_data_t*)stream->abstract; #ifdef PHP_WIN32 int n; #endif + if (close_handle) { if (sslsock->ssl_active) { SSL_shutdown(sslsock->ssl_handle); @@ -282,6 +289,10 @@ static int php_openssl_sockop_close(php_stream *stream, int close_handle TSRMLS_ pefree(sslsock->url_name, php_stream_is_persistent(stream)); } + if (sslsock->reneg) { + pefree(sslsock->reneg, php_stream_is_persistent(stream)); + } + pefree(sslsock, php_stream_is_persistent(stream)); return 0; @@ -297,6 +308,122 @@ 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 inline void limit_handshake_reneg(const SSL *ssl) /* {{{ */ +{ + php_stream *stream; + php_openssl_netstream_data_t *sslsock; + struct timeval now; + long elapsed_time; + + stream = php_openssl_get_stream_from_ssl_handle(ssl); + sslsock = (php_openssl_netstream_data_t*)stream->abstract; + gettimeofday(&now, NULL); + + /* The initial handshake is never rate-limited */ + if (sslsock->reneg->prev_handshake == 0) { + sslsock->reneg->prev_handshake = now.tv_sec; + return; + } + + elapsed_time = (now.tv_sec - sslsock->reneg->prev_handshake); + sslsock->reneg->prev_handshake = now.tv_sec; + sslsock->reneg->tokens -= (elapsed_time * (sslsock->reneg->limit / sslsock->reneg->window)); + + if (sslsock->reneg->tokens < 0) { + sslsock->reneg->tokens = 0; + } + ++sslsock->reneg->tokens; + + /* The token level exceeds our allowed limit */ + if (sslsock->reneg->tokens > sslsock->reneg->limit) { + zval **val; + + TSRMLS_FETCH(); + + sslsock->reneg->should_close = 1; + + if (stream->context && SUCCESS == php_stream_context_get_option(stream->context, + "ssl", "reneg_limit_callback", &val) + ) { + zval *param, **params[1], *retval; + + MAKE_STD_ZVAL(param); + php_stream_to_zval(stream, param); + params[0] = ¶m; + + /* Closing the stream inside this callback would segfault! */ + stream->flags |= PHP_STREAM_FLAG_NO_FCLOSE; + if (FAILURE == call_user_function_ex(EG(function_table), NULL, *val, &retval, 1, params, 0, NULL TSRMLS_CC)) { + php_error(E_WARNING, "SSL: failed invoking reneg limit notification callback"); + } + stream->flags ^= PHP_STREAM_FLAG_NO_FCLOSE; + + /* If the reneg_limit_callback returned true don't auto-close */ + if (retval != NULL && Z_TYPE_P(retval) == IS_BOOL && Z_BVAL_P(retval) == 1) { + sslsock->reneg->should_close = 0; + } + + FREE_ZVAL(param); + if (retval != NULL) { + zval_ptr_dtor(&retval); + } + } else { + php_error_docref(NULL TSRMLS_CC, E_WARNING, + "SSL: client-initiated handshake rate limit exceeded by peer"); + } + } +} +/* }}} */ + +static void php_openssl_info_callback(const SSL *ssl, int where, int ret) /* {{{ */ +{ + /* Rate-limit client-initiated handshake renegotiation to prevent DoS */ + if (where & SSL_CB_HANDSHAKE_START) { + limit_handshake_reneg(ssl); + } +} +/* }}} */ + +static inline void init_handshake_limiting(php_stream *stream, php_openssl_netstream_data_t *sslsock) /* {{{ */ +{ + zval **val; + long limit = DEFAULT_RENEG_LIMIT; + long window = DEFAULT_RENEG_WINDOW; + + if (stream->context && + SUCCESS == php_stream_context_get_option(stream->context, + "ssl", "reneg_limit", &val) + ) { + convert_to_long(*val); + limit = Z_LVAL_PP(val); + } + + /* No renegotiation rate-limiting */ + if (limit < 0) { + return; + } + + if (stream->context && + SUCCESS == php_stream_context_get_option(stream->context, + "ssl", "reneg_window", &val) + ) { + convert_to_long(*val); + window = Z_LVAL_PP(val); + } + + sslsock->reneg = (void*)pemalloc(sizeof(php_openssl_handshake_bucket_t), + php_stream_is_persistent(stream) + ); + + sslsock->reneg->limit = limit; + sslsock->reneg->window = window; + sslsock->reneg->prev_handshake = 0; + sslsock->reneg->tokens = 0; + sslsock->reneg->should_close = 0; + + SSL_CTX_set_info_callback(sslsock->ctx, php_openssl_info_callback); +} +/* }}} */ static const SSL_METHOD *php_select_crypto_method(long method_value, int is_client TSRMLS_DC) { @@ -483,6 +610,10 @@ static inline int php_openssl_setup_crypto(php_stream *stream, SSL_set_mode(sslsock->ssl_handle, mode | SSL_MODE_RELEASE_BUFFERS); #endif + if (!sslsock->is_client) { + init_handshake_limiting(stream, sslsock); + } + if (!SSL_set_fd(sslsock->ssl_handle, sslsock->s.socket)) { handle_ssl_error(stream, 0, 1 TSRMLS_CC); }