Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions ext/openssl/openssl.c
Original file line number Diff line number Diff line change
Expand Up @@ -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];
Expand Down
4 changes: 4 additions & 0 deletions ext/openssl/php_openssl.h
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
9 changes: 9 additions & 0 deletions ext/openssl/php_openssl_structs.h
Original file line number Diff line number Diff line change
Expand Up @@ -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!
Expand All @@ -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;
Expand Down
89 changes: 89 additions & 0 deletions ext/openssl/tests/stream_server_reneg_limit.phpt
Original file line number Diff line number Diff line change
@@ -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)
135 changes: 133 additions & 2 deletions ext/openssl/xp_ssl.c
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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));

Expand All @@ -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);
Expand Down Expand Up @@ -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;
Expand All @@ -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)
{
Expand Down Expand Up @@ -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);
}
Expand Down