diff --git a/ext/standard/ftp_fopen_wrapper.c b/ext/standard/ftp_fopen_wrapper.c index c1f8fe7424b9..8433f605396f 100644 --- a/ext/standard/ftp_fopen_wrapper.c +++ b/ext/standard/ftp_fopen_wrapper.c @@ -123,6 +123,14 @@ static int php_stream_ftp_stream_close(php_stream_wrapper *wrapper, php_stream * } /* }}} */ +/* The components of a php_uri may be interned or shared, so they are decoded into a new string. */ +static zend_string *php_ftp_url_decode(const zend_string *component) +{ + zend_string *decoded = zend_string_alloc(ZSTR_LEN(component), false); + ZSTR_LEN(decoded) = php_raw_url_decode_ex(ZSTR_VAL(decoded), ZSTR_VAL(component), ZSTR_LEN(component)); + return decoded; +} + /* {{{ php_ftp_fopen_connect */ static php_stream *php_ftp_fopen_connect(php_stream_wrapper *wrapper, const char *path, const char *mode, int options, zend_string **opened_path, php_stream_context *context, php_stream **preuseid, @@ -233,11 +241,12 @@ static php_stream *php_ftp_fopen_connect(php_stream_wrapper *wrapper, const char #endif } -#define PHP_FTP_CNTRL_CHK(val, val_len, err_msg) { \ - unsigned char *s = (unsigned char *) val, *e = (unsigned char *) s + val_len; \ +#define PHP_FTP_CNTRL_CHK(val, err_msg) { \ + unsigned char *s = (unsigned char *) ZSTR_VAL(val), *e = s + ZSTR_LEN(val); \ while (s < e) { \ - if (iscntrl((unsigned char)*s)) { \ - php_stream_wrapper_log_error(wrapper, options, err_msg, val); \ + if (iscntrl(*s)) { \ + php_stream_wrapper_log_error(wrapper, options, err_msg, ZSTR_VAL(val)); \ + zend_string_efree(val); \ goto connect_errexit; \ } \ s++; \ @@ -246,11 +255,12 @@ static php_stream *php_ftp_fopen_connect(php_stream_wrapper *wrapper, const char /* send the user name */ if (resource->user != NULL) { - ZSTR_LEN(resource->user) = php_raw_url_decode(ZSTR_VAL(resource->user), ZSTR_LEN(resource->user)); + zend_string *user = php_ftp_url_decode(resource->user); - PHP_FTP_CNTRL_CHK(ZSTR_VAL(resource->user), ZSTR_LEN(resource->user), "Invalid login %s") + PHP_FTP_CNTRL_CHK(user, "Invalid login %s") - php_stream_printf(stream, "USER %s\r\n", ZSTR_VAL(resource->user)); + php_stream_printf(stream, "USER %s\r\n", ZSTR_VAL(user)); + zend_string_efree(user); } else { php_stream_write_string(stream, "USER anonymous\r\n"); } @@ -263,11 +273,12 @@ static php_stream *php_ftp_fopen_connect(php_stream_wrapper *wrapper, const char php_stream_notify_info(context, PHP_STREAM_NOTIFY_AUTH_REQUIRED, tmp_line, 0); if (resource->password != NULL) { - ZSTR_LEN(resource->password) = php_raw_url_decode(ZSTR_VAL(resource->password), ZSTR_LEN(resource->password)); + zend_string *password = php_ftp_url_decode(resource->password); - PHP_FTP_CNTRL_CHK(ZSTR_VAL(resource->password), ZSTR_LEN(resource->password), "Invalid password %s") + PHP_FTP_CNTRL_CHK(password, "Invalid password %s") - php_stream_printf(stream, "PASS %s\r\n", ZSTR_VAL(resource->password)); + php_stream_printf(stream, "PASS %s\r\n", ZSTR_VAL(password)); + zend_string_efree(password); } else { /* if the user has configured who they are, send that as the password */ diff --git a/ext/standard/http_fopen_wrapper.c b/ext/standard/http_fopen_wrapper.c index 9f4582aa3eb1..cd62560c9135 100644 --- a/ext/standard/http_fopen_wrapper.c +++ b/ext/standard/http_fopen_wrapper.c @@ -161,6 +161,32 @@ static bool check_has_header(const char *headers, const char *header) { return 0; } +/* The components of a php_uri may be interned or shared, so they are decoded into a new string. */ +static zend_string *php_stream_http_url_decode(const zend_string *component) +{ + zend_string *decoded = zend_string_alloc(ZSTR_LEN(component), false); + ZSTR_LEN(decoded) = php_url_decode_ex(ZSTR_VAL(decoded), ZSTR_VAL(component), ZSTR_LEN(component)); + return decoded; +} + +static bool php_stream_http_url_component_has_cntrl(const zend_string *component) +{ + if (!component) { + return false; + } + + zend_string *decoded = php_stream_http_url_decode(component); + bool has_cntrl = false; + for (size_t i = 0; i < ZSTR_LEN(decoded); i++) { + if (iscntrl((unsigned char) ZSTR_VAL(decoded)[i])) { + has_cntrl = true; + break; + } + } + zend_string_efree(decoded); + return has_cntrl; +} + static zend_result php_stream_handle_proxy_authorization_header(const char *s, smart_str *header) { const char *p; @@ -816,15 +842,16 @@ static php_stream *php_stream_url_wrap_http_ex(php_stream_wrapper *wrapper, smart_str scratch = {0}; /* decode the strings first */ - ZSTR_LEN(resource->user) = php_url_decode(ZSTR_VAL(resource->user), ZSTR_LEN(resource->user)); - - smart_str_append(&scratch, resource->user); + zend_string *decoded = php_stream_http_url_decode(resource->user); + smart_str_append(&scratch, decoded); + zend_string_efree(decoded); smart_str_appendc(&scratch, ':'); /* Note: password is optional! */ if (resource->password) { - ZSTR_LEN(resource->password) = php_url_decode(ZSTR_VAL(resource->password), ZSTR_LEN(resource->password)); - smart_str_append(&scratch, resource->password); + decoded = php_stream_http_url_decode(resource->password); + smart_str_append(&scratch, decoded); + zend_string_efree(decoded); } zend_string *scratch_str = smart_str_extract(&scratch); @@ -1120,24 +1147,13 @@ static php_stream *php_stream_url_wrap_http_ex(php_stream_wrapper *wrapper, char *loc_path = NULL; if (*header_info.location != '/') { if (header_info.location_len > 1 && resource->path) { - char *s = strrchr(ZSTR_VAL(resource->path), '/'); - if (!s) { - s = ZSTR_VAL(resource->path); - if (!ZSTR_LEN(resource->path)) { - zend_string_release_ex(resource->path, 0); - resource->path = ZSTR_INIT_LITERAL("/", 0); - s = ZSTR_VAL(resource->path); - } else { - *s = '/'; - } - } - s[1] = '\0'; - if (resource->path && - ZSTR_VAL(resource->path)[0] == '/' && - ZSTR_VAL(resource->path)[1] == '\0') { - spprintf(&loc_path, 0, "%s%s", ZSTR_VAL(resource->path), header_info.location); + /* directory of the current path, up to and including its last '/' */ + const char *dir = ZSTR_VAL(resource->path); + const char *s = strrchr(dir, '/'); + if (!s || s == dir) { + spprintf(&loc_path, 0, "/%s", header_info.location); } else { - spprintf(&loc_path, 0, "%s/%s", ZSTR_VAL(resource->path), header_info.location); + spprintf(&loc_path, 0, "%.*s/%s", (int) (s - dir + 1), dir, header_info.location); } } else { spprintf(&loc_path, 0, "/%s", header_info.location); @@ -1177,26 +1193,15 @@ static php_stream *php_stream_url_wrap_http_ex(php_stream_wrapper *wrapper, php_uri_struct_free(resource); resource = new_resource; -#define CHECK_FOR_CNTRL_CHARS(val) { \ - if (val) { \ - unsigned char *s, *e; \ - ZSTR_LEN(val) = php_url_decode(ZSTR_VAL(val), ZSTR_LEN(val)); \ - s = (unsigned char*)ZSTR_VAL(val); e = s + ZSTR_LEN(val); \ - while (s < e) { \ - if (iscntrl(*s)) { \ - php_stream_wrapper_log_error(wrapper, options, "Invalid redirect URL! %s", new_path); \ - efree(new_path); \ - goto out; \ - } \ - s++; \ - } \ - } \ -} /* check for control characters in login, password & path */ if (strncasecmp(new_path, "http://", sizeof("http://") - 1) || strncasecmp(new_path, "https://", sizeof("https://") - 1)) { - CHECK_FOR_CNTRL_CHARS(resource->user); - CHECK_FOR_CNTRL_CHARS(resource->password); - CHECK_FOR_CNTRL_CHARS(resource->path); + if (php_stream_http_url_component_has_cntrl(resource->user) + || php_stream_http_url_component_has_cntrl(resource->password) + || php_stream_http_url_component_has_cntrl(resource->path)) { + php_stream_wrapper_log_error(wrapper, options, "Invalid redirect URL! %s", new_path); + efree(new_path); + goto out; + } } int new_flags = HTTP_WRAPPER_REDIRECTED | (flags & HTTP_WRAPPER_STRIP_AUTH); if (!same_origin) { diff --git a/ext/standard/tests/http/http_redirect_userinfo_whatwg_decode.phpt b/ext/standard/tests/http/http_redirect_userinfo_whatwg_decode.phpt new file mode 100644 index 000000000000..324472420a8f --- /dev/null +++ b/ext/standard/tests/http/http_redirect_userinfo_whatwg_decode.phpt @@ -0,0 +1,42 @@ +--TEST-- +Redirect userinfo is decoded into a new string with the WHATWG URL parser +--FILE-- + ['follow_location' => 1, 'uri_parser_class' => \Uri\WhatWg\Url::class]]); +echo file_get_contents("http://{{ ADDR }}/a", false, $ctx), "\n"; +var_dump(bin2hex(chr(43))); +CODE; + +include sprintf("%s/../../../openssl/tests/ServerClientTestCase.inc", __DIR__); +ServerClientTestCase::getInstance()->run($clientCode, $serverCode); +?> +--EXPECT-- +Authorization: Basic IDo= +string(2) "2b"