]> granicus.if.org Git - php/commitdiff
Mitigate client-initiated SSL renegotiation DoS
authorDaniel Lowrey <rdlowrey@php.net>
Thu, 20 Feb 2014 23:26:55 +0000 (16:26 -0700)
committerDaniel Lowrey <rdlowrey@php.net>
Fri, 21 Feb 2014 13:31:56 +0000 (06:31 -0700)
ext/openssl/openssl.c
ext/openssl/php_openssl.h
ext/openssl/php_openssl_structs.h
ext/openssl/tests/stream_server_reneg_limit.phpt [new file with mode: 0644]
ext/openssl/xp_ssl.c

index 8b77d28ad1e61039a71fcb00b198bc5ac57ba3e6..c813a7ea29c394db4c5028e18fd5f1f29ce9ae8a 100755 (executable)
@@ -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];
index cab787afedd165560928e2a293d5439840ab18bd..a823d30bd8c5a73f1f1457f4e97e8aa2aefea0a5 100644 (file)
@@ -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);
index 13f8f320f8ced053e741823f9f6d82617276bc92..562c756cd3516c55080f36876b0414e804232cb0 100644 (file)
 #include "php_network.h"
 #include <openssl/ssl.h>
 
+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 (file)
index 0000000..134d3cb
--- /dev/null
@@ -0,0 +1,89 @@
+--TEST--
+TLS server rate-limits client-initiated 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 = 'ssl://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' => __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)
index 7104cb07b6f7a1d6b534dbc1a385d324337a042c..cf00aabfa7b92deac1c756a55d27d62709e5e31f 100644 (file)
@@ -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] = &param;
+
+                       /* 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)
 {
@@ -480,6 +607,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);
        }