-
Notifications
You must be signed in to change notification settings - Fork 7.8k
Client-Initiated Renegotiation DoS Vulnerability #486
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,88 @@ | ||
--TEST-- | ||
Encrypted server rate-limits client-initiated TLS 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 = 'tcp://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' => dirname(__FILE__) . '/bug54992.pem', | ||
'max_handshake_rate' => 1 | ||
]])); | ||
|
||
$pid = pcntl_fork(); | ||
if ($pid == -1) { | ||
die('could not fork'); | ||
} elseif ($pid) { | ||
|
||
$cmd = 'openssl s_client -connect localhost: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\n"); | ||
|
||
$lines = []; | ||
while(!feof($stderr)) { | ||
fgets($stderr); | ||
} | ||
|
||
fclose($stdin); | ||
fclose($stdout); | ||
fclose($stderr); | ||
proc_close($process); | ||
pcntl_wait($status); | ||
|
||
} else { | ||
|
||
$clients = []; | ||
while (1) { | ||
$r = array_merge([$server], $clients); | ||
$w = $e = []; | ||
stream_select($r, $w, $e, $timeout=1); | ||
|
||
foreach ($r as $sock) { | ||
if ($sock === $server && ($client = @stream_socket_accept($server, $timeout = 1))) { | ||
stream_socket_enable_crypto($client, TRUE, STREAM_CRYPTO_METHOD_TLS_SERVER); | ||
$clientId = (int) $client; | ||
$clients[$clientId] = $client; | ||
} else { | ||
$clientId = (int) $client; | ||
$buffer = @fread($sock, 1024); | ||
|
||
if (strlen($buffer)) { | ||
continue; | ||
} elseif (feof($sock)) { | ||
$meta = !empty(stream_get_meta_data($sock)['wrapper_data']['handshake_limit_exceeded']); | ||
var_dump($meta); | ||
fclose($sock); | ||
unset($clients[$clientId]); | ||
break 2; | ||
} | ||
} | ||
} | ||
} | ||
} | ||
--EXPECTF-- | ||
bool(true) |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -39,6 +39,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); | ||
|
||
/* This implementation is very closely tied to the that of the native | ||
|
@@ -58,6 +59,9 @@ typedef struct _php_openssl_netstream_data_t { | |
char *sni; | ||
unsigned state_set:1; | ||
unsigned _spare:31; | ||
struct timeval *handshake_history; | ||
int handshake_status; | ||
int handshake_limit; | ||
} php_openssl_netstream_data_t; | ||
|
||
php_stream_ops php_openssl_socket_ops; | ||
|
@@ -215,7 +219,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->handshake_status < 0) { | ||
/* 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)); | ||
|
||
|
@@ -288,6 +298,9 @@ static int php_openssl_sockop_close(php_stream *stream, int close_handle TSRMLS_ | |
if (sslsock->sni) { | ||
pefree(sslsock->sni, php_stream_is_persistent(stream)); | ||
} | ||
if (sslsock->handshake_history) { | ||
pefree(sslsock->handshake_history, php_stream_is_persistent(stream)); | ||
} | ||
pefree(sslsock, php_stream_is_persistent(stream)); | ||
|
||
return 0; | ||
|
@@ -303,6 +316,63 @@ 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 void php_openssl_info_callback(const SSL *ssl, int where, int ret) | ||
{ | ||
zend_bool is_limit_reached = 0; | ||
php_stream *stream; | ||
php_openssl_netstream_data_t *sslsock; | ||
|
||
stream = php_openssl_get_stream_from_ssl_handle(ssl); | ||
sslsock = (php_openssl_netstream_data_t*)stream->abstract; | ||
|
||
if (!(where & SSL_CB_HANDSHAKE_START) || sslsock->is_client) { | ||
return; | ||
} else if (sslsock->handshake_limit == 0) { | ||
if (sslsock->handshake_status == 0) { | ||
sslsock->handshake_status = 1; | ||
} else { | ||
is_limit_reached = 1; | ||
} | ||
} else { | ||
int i; | ||
struct timeval now; | ||
zend_bool carry = 0; | ||
|
||
gettimeofday(&now, NULL); | ||
for (i = sslsock->handshake_status - 1; i >= 0; i--) { | ||
time_t secsago = now.tv_sec - sslsock->handshake_history[i].tv_sec; | ||
if (secsago > 1 || (secsago == 1 && sslsock->handshake_history[i].tv_usec <= now.tv_usec)) { | ||
sslsock->handshake_status -= 1; | ||
} else if (i < sslsock->handshake_limit - 1) { | ||
carry = 1; | ||
} else { | ||
is_limit_reached = 1; | ||
break; | ||
} | ||
|
||
if (carry) { | ||
sslsock->handshake_history[i + 1] = sslsock->handshake_history[i]; | ||
} | ||
} | ||
|
||
if (!is_limit_reached) { | ||
sslsock->handshake_history[0] = now; | ||
sslsock->handshake_status += 1; | ||
} | ||
} | ||
|
||
if (is_limit_reached) { | ||
zval *data, *arr; | ||
MAKE_STD_ZVAL(data); | ||
ZVAL_BOOL(data, 1); | ||
MAKE_STD_ZVAL(arr); | ||
array_init(arr); | ||
add_assoc_zval(arr, "handshake_limit_exceeded", data); | ||
stream->wrapperdata = arr; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. What if |
||
sslsock->handshake_status = -1; | ||
} | ||
} | ||
/* }}} */ | ||
|
||
static inline int php_openssl_setup_crypto(php_stream *stream, | ||
php_openssl_netstream_data_t *sslsock, | ||
|
@@ -311,6 +381,7 @@ static inline int php_openssl_setup_crypto(php_stream *stream, | |
{ | ||
const SSL_METHOD *method; | ||
long ssl_ctx_options = SSL_OP_ALL; | ||
zval **handshake_limit_val; | ||
|
||
if (sslsock->ssl_handle) { | ||
if (sslsock->s.is_blocked) { | ||
|
@@ -415,6 +486,19 @@ static inline int php_openssl_setup_crypto(php_stream *stream, | |
return -1; | ||
} | ||
|
||
/* Enable callback for client renegotiation DoS protection */ | ||
if (stream->context && SUCCESS == php_stream_context_get_option(stream->context, "ssl", "max_handshake_rate", &handshake_limit_val )) { | ||
sslsock->handshake_limit = Z_LVAL_PP(handshake_limit_val); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. What if |
||
if (sslsock->handshake_limit >= 0) { | ||
sslsock->handshake_history = (struct timeval*)pemalloc( | ||
(size_t)(sslsock->handshake_limit * sizeof(struct timeval)), | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. There's |
||
php_stream_is_persistent(stream) | ||
); | ||
sslsock->handshake_status = 0; | ||
SSL_CTX_set_info_callback(sslsock->ctx, php_openssl_info_callback); | ||
} | ||
} | ||
|
||
if (!SSL_set_fd(sslsock->ssl_handle, sslsock->s.socket)) { | ||
handle_ssl_error(stream, 0, 1 TSRMLS_CC); | ||
} | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Both
stream
andsslock
, never ever could be NULL?