mirror of
https://github.com/php/php-src.git
synced 2024-09-22 18:37:25 +00:00
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:
commit
5389d0963c
@ -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];
|
||||
|
@ -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);
|
||||
|
@ -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;
|
||||
|
89
ext/openssl/tests/stream_server_reneg_limit.phpt
Normal file
89
ext/openssl/tests/stream_server_reneg_limit.phpt
Normal 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)
|
@ -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);
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user