Merge branch 'reneg-limit' of https://github.com/rdlowrey/php-src into PHP-5.6

* 'reneg-limit' of https://github.com/rdlowrey/php-src:
  Mitigate client-initiated SSL renegotiation DoS
This commit is contained in:
Daniel Lowrey 2014-02-21 09:13:55 -07:00
commit 5389d0963c
5 changed files with 241 additions and 2 deletions

View File

@ -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];

View 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);

View File

@ -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;

View File

@ -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)

View 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)
{
@ -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);
}