summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorDaniel Lowrey <rdlowrey@php.net>2014-02-20 16:26:55 -0700
committerDaniel Lowrey <rdlowrey@php.net>2014-02-21 06:31:56 -0700
commitb6edbd5897bfa18d6a8ae9a9662efa34abfba39e (patch)
tree3de7eeafdc6edb5524f1ed8c6867f57bc21163b8
parentdcf27a1f6f205a22ecff1e537de6b13b0f1187ad (diff)
downloadphp-git-b6edbd5897bfa18d6a8ae9a9662efa34abfba39e.tar.gz
Mitigate client-initiated SSL renegotiation DoS
-rwxr-xr-xext/openssl/openssl.c6
-rw-r--r--ext/openssl/php_openssl.h4
-rw-r--r--ext/openssl/php_openssl_structs.h9
-rw-r--r--ext/openssl/tests/stream_server_reneg_limit.phpt89
-rw-r--r--ext/openssl/xp_ssl.c135
5 files changed, 241 insertions, 2 deletions
diff --git a/ext/openssl/openssl.c b/ext/openssl/openssl.c
index 8b77d28ad1..c813a7ea29 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 cab787afed..a823d30bd8 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 13f8f320f8..562c756cd3 100644
--- a/ext/openssl/php_openssl_structs.h
+++ b/ext/openssl/php_openssl_structs.h
@@ -22,6 +22,14 @@
#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
index 0000000000..134d3cb601
--- /dev/null
+++ b/ext/openssl/tests/stream_server_reneg_limit.phpt
@@ -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)
diff --git a/ext/openssl/xp_ssl.c b/ext/openssl/xp_ssl.c
index 7104cb07b6..cf00aabfa7 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] = &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);
}