diff --git a/UPGRADING b/UPGRADING index 4836c26a2870..e9aeab984477 100644 --- a/UPGRADING +++ b/UPGRADING @@ -315,6 +315,10 @@ PHP 8.6 UPGRADE NOTES endianness modifier. A name starting with these characters is unaffected when a repeater precedes it, as in "s1is_url) { + case STREAM_IS_URL_NEVER: + return false; + case STREAM_IS_URL_ALWAYS: + return true; + case STREAM_IS_URL_SOMETIMES: + ZEND_ASSERT(inner_wrapper->wops->stream_is_url != NULL); + return (inner_wrapper->wops->stream_is_url)(inner_wrapper, inner_path, context); + default: ZEND_UNREACHABLE(); + } +} + static const php_stream_wrapper_ops bzip2_stream_wops = { _php_stream_bz2open, NULL, /* close */ @@ -262,13 +292,14 @@ static const php_stream_wrapper_ops bzip2_stream_wops = { NULL, /* rename */ NULL, /* mkdir */ NULL, /* rmdir */ - NULL + NULL, + bz2_stream_is_url, }; static const php_stream_wrapper php_stream_bzip2_wrapper = { &bzip2_stream_wops, NULL, - 0 /* is_url */ + STREAM_IS_URL_SOMETIMES, }; static void php_bz2_error(INTERNAL_FUNCTION_PARAMETERS, int); diff --git a/ext/openssl/xp_ssl.c b/ext/openssl/xp_ssl.c index 269de9545388..196b42c2853d 100644 --- a/ext/openssl/xp_ssl.c +++ b/ext/openssl/xp_ssl.c @@ -895,7 +895,18 @@ static long php_openssl_load_stream_cafile(X509_STORE *cert_store, const char *c // TODO: no stream and no wrapper, cannot use php_stream_warn(stream, ReadFailed, ...) nor php_stream_wrapper_log_error() php_error(E_WARNING, "failed loading cafile stream: `%s'", cafile); return 0; - } else if (stream->wrapper->is_url) { + } + bool is_url = false; + if (stream->wrapper->is_url == STREAM_IS_URL_ALWAYS) { + is_url = true; + } else if (stream->wrapper->is_url == STREAM_IS_URL_NEVER) { + is_url = false; + } else { + ZEND_ASSERT(stream->wrapper->is_url == STREAM_IS_URL_SOMETIMES); + ZEND_ASSERT(stream->wrapper->wops->stream_is_url != NULL); + is_url = (stream->wrapper->wops->stream_is_url)(stream->wrapper, cafile, NULL); + } + if (is_url) { php_stream_warn(stream, PermissionDenied, "remote cafile streams are disabled for security purposes"); php_stream_close(stream); return 0; diff --git a/ext/phar/stream.c b/ext/phar/stream.c index 335f8a073424..f867866281e0 100644 --- a/ext/phar/stream.c +++ b/ext/phar/stream.c @@ -43,13 +43,14 @@ static const php_stream_wrapper_ops phar_stream_wops = { phar_wrapper_rename, /* rename */ phar_wrapper_mkdir, /* create directory */ phar_wrapper_rmdir, /* remove directory */ - NULL + NULL, + NULL, /* is_url, unneeded since phars are never for URLs */ }; const php_stream_wrapper php_stream_phar_wrapper = { &phar_stream_wops, NULL, - 0 /* is_url */ + STREAM_IS_URL_NEVER, }; /** diff --git a/ext/standard/ftp_fopen_wrapper.c b/ext/standard/ftp_fopen_wrapper.c index cf529e40085b..2f5786069666 100644 --- a/ext/standard/ftp_fopen_wrapper.c +++ b/ext/standard/ftp_fopen_wrapper.c @@ -1182,11 +1182,12 @@ static const php_stream_wrapper_ops ftp_stream_wops = { php_stream_ftp_rename, /* rename */ php_stream_ftp_mkdir, /* mkdir */ php_stream_ftp_rmdir, /* rmdir */ - NULL + NULL, + NULL, /* is_url, unneeded since FTP is always for URLs */ }; PHPAPI const php_stream_wrapper php_stream_ftp_wrapper = { &ftp_stream_wops, NULL, - 1 /* is_url */ + STREAM_IS_URL_ALWAYS, }; diff --git a/ext/standard/http_fopen_wrapper.c b/ext/standard/http_fopen_wrapper.c index 4a0f95062bcd..d4f6dea17c8b 100644 --- a/ext/standard/http_fopen_wrapper.c +++ b/ext/standard/http_fopen_wrapper.c @@ -1245,11 +1245,12 @@ static const php_stream_wrapper_ops http_stream_wops = { NULL, /* rename */ NULL, /* mkdir */ NULL, /* rmdir */ - NULL + NULL, + NULL, /* is_url, unneeded since HTTP is always for URLs */ }; PHPAPI const php_stream_wrapper php_stream_http_wrapper = { &http_stream_wops, NULL, - 1 /* is_url */ + STREAM_IS_URL_ALWAYS, }; diff --git a/ext/standard/php_fopen_wrapper.c b/ext/standard/php_fopen_wrapper.c index cca9445801f0..4aaa04745653 100644 --- a/ext/standard/php_fopen_wrapper.c +++ b/ext/standard/php_fopen_wrapper.c @@ -481,11 +481,12 @@ static const php_stream_wrapper_ops php_stdio_wops = { NULL, /* rename */ NULL, /* mkdir */ NULL, /* rmdir */ - NULL + NULL, + NULL, /* is_url, unneeded since php is never for URLs */ }; PHPAPI const php_stream_wrapper php_stream_php_wrapper = { &php_stdio_wops, NULL, - 0, /* is_url */ + STREAM_IS_URL_NEVER, }; diff --git a/ext/standard/streamsfuncs.c b/ext/standard/streamsfuncs.c index a03aed1ccdd4..8661484ff937 100644 --- a/ext/standard/streamsfuncs.c +++ b/ext/standard/streamsfuncs.c @@ -1686,9 +1686,12 @@ PHP_FUNCTION(stream_is_local) Z_PARAM_RESOURCE_OR_NULL(zcontext) ZEND_PARSE_PARAMETERS_END(); + const char *path; + if (Z_TYPE_P(zstream) == IS_RESOURCE) { php_stream_from_zval(stream, zstream); wrapper = stream->wrapper; + path = stream->orig_path; } else { if (!try_convert_to_string(zstream)) { RETURN_THROWS(); @@ -1696,11 +1699,27 @@ PHP_FUNCTION(stream_is_local) php_stream_error_operation_begin(); context = php_stream_context_from_zval(zcontext, 0); - wrapper = php_stream_locate_url_wrapper(Z_STRVAL_P(zstream), NULL, 0); + + path = Z_STRVAL_P(zstream); + wrapper = php_stream_locate_url_wrapper(path, NULL, 0); php_stream_error_operation_end(context); } - RETURN_BOOL(wrapper && wrapper->is_url == 0); + if (!wrapper) { + RETURN_FALSE; + } + switch (wrapper->is_url) { + case STREAM_IS_URL_NEVER: + RETURN_TRUE; + case STREAM_IS_URL_ALWAYS: + RETURN_FALSE; + case STREAM_IS_URL_SOMETIMES: + ZEND_ASSERT(wrapper->wops->stream_is_url != NULL); + RETURN_BOOL( + !(wrapper->wops->stream_is_url)(wrapper, path, NULL) + ); + default: ZEND_UNREACHABLE(); + } } /* }}} */ diff --git a/ext/standard/tests/streams/stream-fopen-exponential.phpt b/ext/standard/tests/streams/stream-fopen-exponential.phpt new file mode 100644 index 000000000000..9c663e3af37e --- /dev/null +++ b/ext/standard/tests/streams/stream-fopen-exponential.phpt @@ -0,0 +1,35 @@ +--TEST-- +fopen() with bz2 and zlib wrappers shouldn't take exponential time +--EXTENSIONS-- +bz2 +zlib +--INI-- +allow_url_fopen=0 +--FILE-- + +--EXPECTF-- +Warning: fopen(): compress.zlib:// wrapper is disabled in the server configuration by allow_url_fopen=0 in %s on line %d + +Warning: fopen(): Failed to open stream: no suitable wrapper could be found in %s on line %d +bool(false) + +Warning: fopen(): compress.zlib:// wrapper is disabled in the server configuration by allow_url_fopen=0 in %s on line %d + +Warning: fopen(): Failed to open stream: no suitable wrapper could be found in %s on line %d +bool(false) + +Warning: fopen(): compress.zlib:// wrapper is disabled in the server configuration by allow_url_fopen=0 in %s on line %d + +Warning: fopen(): Failed to open stream: no suitable wrapper could be found in %s on line %d +bool(false) diff --git a/ext/standard/tests/streams/stream_is_local-bzip2.phpt b/ext/standard/tests/streams/stream_is_local-bzip2.phpt new file mode 100644 index 000000000000..3bd3a065b31b --- /dev/null +++ b/ext/standard/tests/streams/stream_is_local-bzip2.phpt @@ -0,0 +1,65 @@ +--TEST-- +Testing stream_is_local() with bzip2 wrappers +--EXTENSIONS-- +bz2 +--FILE-- + +--EXPECT-- +http://127.0.0.1/example.html +bool(false) + +compress.bzip2://http://127.0.0.1/example.html +bool(false) + +compress.bzip2://compress.bzip2://http://127.0.0.1/example.html +bool(false) + +ftp://127.0.0.1/example.html +bool(false) + +compress.bzip2://ftp://127.0.0.1/example.html +bool(false) + +compress.bzip2://compress.bzip2://ftp://127.0.0.1/example.html +bool(false) + +/etc/os-release +bool(true) + +compress.bzip2:///etc/os-release +bool(true) + +compress.bzip2://compress.bzip2:///etc/os-release +bool(true) + +file://evil.example.com/x +bool(false) + +compress.bzip2://file://evil.example.com/x +bool(true) + +compress.bzip2://compress.bzip2://file://evil.example.com/x +bool(true) diff --git a/ext/standard/tests/streams/stream_is_local-mixed.phpt b/ext/standard/tests/streams/stream_is_local-mixed.phpt new file mode 100644 index 000000000000..04589a8c9ec8 --- /dev/null +++ b/ext/standard/tests/streams/stream_is_local-mixed.phpt @@ -0,0 +1,79 @@ +--TEST-- +Testing stream_is_local() with bz2 and zlib wrappers +--EXTENSIONS-- +bz2 +zlib +--FILE-- + +--EXPECTF-- +http://127.0.0.1/example.html +bool(false) + +compress.bzip2://compress.zlib://http://127.0.0.1/example.html +bool(false) + +compress.zlib://compress.bzip2://http://127.0.0.1/example.html +bool(false) + +%r(compress\.zlib:\/\/compress\.bzip2:\/\/){20}%rhttp://127.0.0.1/example.html +bool(false) + +ftp://127.0.0.1/example.html +bool(false) + +compress.bzip2://compress.zlib://ftp://127.0.0.1/example.html +bool(false) + +compress.zlib://compress.bzip2://ftp://127.0.0.1/example.html +bool(false) + +%r(compress\.zlib:\/\/compress\.bzip2:\/\/){20}%rftp://127.0.0.1/example.html +bool(false) + +/etc/os-release +bool(true) + +compress.bzip2://compress.zlib:///etc/os-release +bool(true) + +compress.zlib://compress.bzip2:///etc/os-release +bool(true) + +%r(compress\.zlib:\/\/compress\.bzip2:\/\/){20}%r/etc/os-release +bool(true) + +file://evil.example.com/x +bool(false) + +compress.bzip2://compress.zlib://file://evil.example.com/x +bool(true) + +compress.zlib://compress.bzip2://file://evil.example.com/x +bool(true) + +%r(compress\.zlib:\/\/compress\.bzip2:\/\/){20}%rfile://evil.example.com/x +bool(true) diff --git a/ext/standard/tests/streams/stream_is_local-zlib.phpt b/ext/standard/tests/streams/stream_is_local-zlib.phpt new file mode 100644 index 000000000000..51fb08d315c7 --- /dev/null +++ b/ext/standard/tests/streams/stream_is_local-zlib.phpt @@ -0,0 +1,65 @@ +--TEST-- +Testing stream_is_local() with zlib wrappers +--EXTENSIONS-- +zlib +--FILE-- + +--EXPECT-- +http://127.0.0.1/example.html +bool(false) + +compress.zlib://http://127.0.0.1/example.html +bool(false) + +compress.zlib://compress.zlib://http://127.0.0.1/example.html +bool(false) + +ftp://127.0.0.1/example.html +bool(false) + +compress.zlib://ftp://127.0.0.1/example.html +bool(false) + +compress.zlib://compress.zlib://ftp://127.0.0.1/example.html +bool(false) + +/etc/os-release +bool(true) + +compress.zlib:///etc/os-release +bool(true) + +compress.zlib://compress.zlib:///etc/os-release +bool(true) + +file://evil.example.com/x +bool(false) + +compress.zlib://file://evil.example.com/x +bool(true) + +compress.zlib://compress.zlib://file://evil.example.com/x +bool(true) diff --git a/ext/zip/zip_stream.c b/ext/zip/zip_stream.c index 429342b36e3d..618bc06c44d2 100644 --- a/ext/zip/zip_stream.c +++ b/ext/zip/zip_stream.c @@ -389,12 +389,13 @@ static const php_stream_wrapper_ops zip_stream_wops = { NULL, /* rename */ NULL, /* mkdir */ NULL, /* rmdir */ - NULL /* metadata */ + NULL, /* metadata */ + NULL, /* is_url, unneeded since ZIPs are never for URLs */ }; const php_stream_wrapper php_stream_zip_wrapper = { &zip_stream_wops, NULL, - 0 /* is_url */ + STREAM_IS_URL_NEVER, }; #endif /* HAVE_ZIP */ diff --git a/ext/zlib/zlib_fopen_wrapper.c b/ext/zlib/zlib_fopen_wrapper.c index 2b922bf1d11e..967db3f66cae 100644 --- a/ext/zlib/zlib_fopen_wrapper.c +++ b/ext/zlib/zlib_fopen_wrapper.c @@ -235,6 +235,38 @@ php_stream *php_stream_gzopen(php_stream_wrapper *wrapper, const char *path, con return NULL; } +static bool gzip_stream_is_url(php_stream_wrapper *wrapper, const char *path, php_stream_context *context) +{ + const char *inner_path = path; + while (*inner_path) { + if (strncasecmp("compress.zlib://", inner_path, strlen("compress.zlib://")) == 0) { + inner_path += strlen("compress.zlib://"); + } else if (strncasecmp("zlib:", inner_path, strlen("zlib:")) == 0) { + inner_path += strlen("zlib:"); + } else { + break; + } + } + /* Disable URL protection, which would cause php_stream_locate_url_wrapper() + * to do its own filtering of inner stream_is_url callbacks; we do that + * check here anyway. */ + php_stream_wrapper *inner_wrapper = php_stream_locate_url_wrapper(inner_path, NULL, STREAM_DISABLE_URL_PROTECTION); + if (inner_wrapper == NULL) { + // No actual target being compressed, so not a URL + return false; + } + switch (inner_wrapper->is_url) { + case STREAM_IS_URL_NEVER: + return false; + case STREAM_IS_URL_ALWAYS: + return true; + case STREAM_IS_URL_SOMETIMES: + ZEND_ASSERT(inner_wrapper->wops->stream_is_url != NULL); + return (inner_wrapper->wops->stream_is_url)(inner_wrapper, inner_path, context); + default: ZEND_UNREACHABLE(); + } +} + static const php_stream_wrapper_ops gzip_stream_wops = { php_stream_gzopen, NULL, /* close */ @@ -246,11 +278,12 @@ static const php_stream_wrapper_ops gzip_stream_wops = { NULL, /* rename */ NULL, /* mkdir */ NULL, /* rmdir */ - NULL + NULL, + gzip_stream_is_url, }; const php_stream_wrapper php_stream_gzip_wrapper = { &gzip_stream_wops, NULL, - 0, /* is_url */ + STREAM_IS_URL_SOMETIMES, }; diff --git a/main/php_streams.h b/main/php_streams.h index fb0c57ecf83d..7b6a2b48f0e8 100644 --- a/main/php_streams.h +++ b/main/php_streams.h @@ -155,12 +155,20 @@ typedef struct _php_stream_wrapper_ops { int (*stream_rmdir)(php_stream_wrapper *wrapper, const char *url, int options, php_stream_context *context); /* Metadata handling */ int (*stream_metadata)(php_stream_wrapper *wrapper, const char *url, int options, void *value, php_stream_context *context); + /* Required if the wrapper is_url is STREAM_IS_URL_SOMETIMES, otherwise ignored */ + bool (*stream_is_url)(php_stream_wrapper *wrapper, const char *url, php_stream_context *context); } php_stream_wrapper_ops; +C23_ENUM(php_stream_wrapper_is_url, uint8_t) { + STREAM_IS_URL_NEVER = 0, + STREAM_IS_URL_ALWAYS = 1, + STREAM_IS_URL_SOMETIMES = 2, +}; + struct _php_stream_wrapper { const php_stream_wrapper_ops *wops; /* operations the wrapper can perform */ void *abstract; /* context for the wrapper */ - int is_url; /* so that PG(allow_url_fopen) can be respected */ + php_stream_wrapper_is_url is_url; /* so that PG(allow_url_fopen) can be respected */ }; #define PHP_STREAM_FLAG_NO_SEEK 0x1 diff --git a/main/streams/glob_wrapper.c b/main/streams/glob_wrapper.c index c7f1145de200..8f33817cf7f1 100644 --- a/main/streams/glob_wrapper.c +++ b/main/streams/glob_wrapper.c @@ -309,11 +309,12 @@ static const php_stream_wrapper_ops php_glob_stream_wrapper_ops = { NULL, NULL, NULL, - NULL + NULL, + NULL, /* is_url, unneeded since globs are never for URLs */ }; const php_stream_wrapper php_glob_stream_wrapper = { &php_glob_stream_wrapper_ops, NULL, - 0 + STREAM_IS_URL_NEVER, }; diff --git a/main/streams/memory.c b/main/streams/memory.c index e76598ed0f46..8e186be4fb64 100644 --- a/main/streams/memory.c +++ b/main/streams/memory.c @@ -778,10 +778,11 @@ PHPAPI const php_stream_wrapper_ops php_stream_rfc2397_wops = { NULL, /* mkdir */ NULL, /* rmdir */ NULL, /* stream_metadata */ + NULL, /* is_url, unneeded since memory streams are always considered to be for URLs */ }; PHPAPI const php_stream_wrapper php_stream_rfc2397_wrapper = { &php_stream_rfc2397_wops, NULL, - 1, /* is_url */ + STREAM_IS_URL_ALWAYS, }; diff --git a/main/streams/plain_wrapper.c b/main/streams/plain_wrapper.c index eb9b81b6e2c5..534e4d4e9b9c 100644 --- a/main/streams/plain_wrapper.c +++ b/main/streams/plain_wrapper.c @@ -1717,12 +1717,13 @@ static const php_stream_wrapper_ops php_plain_files_wrapper_ops = { php_plain_files_rename, php_plain_files_mkdir, php_plain_files_rmdir, - php_plain_files_metadata + php_plain_files_metadata, + NULL, /* is_url, unneeded since local files are never for URLs */ }; /* TODO: We have to make php_plain_files_wrapper writable to support SWOOLE */ PHPAPI /*const*/ php_stream_wrapper php_plain_files_wrapper = { &php_plain_files_wrapper_ops, NULL, - 0 + STREAM_IS_URL_NEVER, }; diff --git a/main/streams/streams.c b/main/streams/streams.c index 7cd63f0038d3..78b4ef7dd934 100644 --- a/main/streams/streams.c +++ b/main/streams/streams.c @@ -1968,20 +1968,34 @@ PHPAPI php_stream_wrapper *php_stream_locate_url_wrapper(const char *path, const return plain_files_wrapper; } - if (wrapper && wrapper->is_url && + if (wrapper && (options & STREAM_DISABLE_URL_PROTECTION) == 0 && (!PG(allow_url_fopen) || (((options & STREAM_OPEN_FOR_INCLUDE) || - PG(in_user_include)) && !PG(allow_url_include)))) { - if (options & REPORT_ERRORS) { - /* protocol[n] probably isn't '\0' */ - if (!PG(allow_url_fopen)) { - php_error_docref(NULL, E_WARNING, "%.*s:// wrapper is disabled in the server configuration by allow_url_fopen=0", (int)n, protocol); - } else { - php_error_docref(NULL, E_WARNING, "%.*s:// wrapper is disabled in the server configuration by allow_url_include=0", (int)n, protocol); + PG(in_user_include)) && !PG(allow_url_include))) + ) { + bool is_url = false; + if (wrapper->is_url == STREAM_IS_URL_ALWAYS) { + is_url = true; + } else if (wrapper->is_url == STREAM_IS_URL_NEVER) { + is_url = false; + } else { + ZEND_ASSERT(wrapper->is_url == STREAM_IS_URL_SOMETIMES); + ZEND_ASSERT(wrapper->wops->stream_is_url != NULL); + is_url = (wrapper->wops->stream_is_url)(wrapper, path, NULL); + } + + if (is_url) { + if (options & REPORT_ERRORS) { + /* protocol[n] probably isn't '\0' */ + if (!PG(allow_url_fopen)) { + php_error_docref(NULL, E_WARNING, "%.*s:// wrapper is disabled in the server configuration by allow_url_fopen=0", (int)n, protocol); + } else { + php_error_docref(NULL, E_WARNING, "%.*s:// wrapper is disabled in the server configuration by allow_url_include=0", (int)n, protocol); + } } + return NULL; } - return NULL; } return wrapper; @@ -2127,11 +2141,23 @@ PHPAPI php_stream *_php_stream_open_wrapper_ex(const char *path, const char *mod "Failed to open stream: no suitable wrapper could be found"); goto cleanup_no_wrapper_name; } - if ((options & STREAM_USE_URL) && !wrapper->is_url) { - php_stream_wrapper_warn(wrapper, context, options, - ProtocolUnsupported, - "This function may only be used against URLs"); - goto cleanup_no_wrapper_name; + if (options & STREAM_USE_URL) { + bool is_url = false; + if (wrapper->is_url == STREAM_IS_URL_ALWAYS) { + is_url = true; + } else if (wrapper->is_url == STREAM_IS_URL_NEVER) { + is_url = false; + } else { + ZEND_ASSERT(wrapper->is_url == STREAM_IS_URL_SOMETIMES); + ZEND_ASSERT(wrapper->wops->stream_is_url != NULL); + is_url = (wrapper->wops->stream_is_url)(wrapper, path, context); + } + if (!is_url) { + php_stream_wrapper_warn(wrapper, context, options, + ProtocolUnsupported, + "This function may only be used against URLs"); + goto cleanup_no_wrapper_name; + } } if (!wrapper->wops->stream_opener) { diff --git a/main/streams/userspace.c b/main/streams/userspace.c index 9b6f283c075d..93cac4fa1ff6 100644 --- a/main/streams/userspace.c +++ b/main/streams/userspace.c @@ -61,7 +61,8 @@ static const php_stream_wrapper_ops user_stream_wops = { user_wrapper_rename, user_wrapper_mkdir, user_wrapper_rmdir, - user_wrapper_metadata + user_wrapper_metadata, + NULL, /* is_url - userspace streams that are only sometimes local are not (yet) supported */ }; @@ -296,10 +297,11 @@ static php_stream *user_wrapper_opener(php_stream_wrapper *wrapper, const char * /* if the user stream was registered as local and we are in include context, we add allow_url_include restrictions to allow_url_fopen ones */ - /* we need only is_url == 0 here since if is_url == 1 and remote wrappers - were restricted we wouldn't get here */ + /* we need only is_url == STREAM_IS_URL_NEVER here since if + * is_url == STREAM_IS_URL_ALWAYS and remote wrappers were restricted we + * wouldn't get here; user streams do not yet support STREAM_IS_URL_SOMETIMES */ old_in_user_include = PG(in_user_include); - if(uwrap->wrapper.is_url == 0 && + if(uwrap->wrapper.is_url == STREAM_IS_URL_NEVER && (options & STREAM_OPEN_FOR_INCLUDE) && !PG(allow_url_include)) { PG(in_user_include) = 1; @@ -473,7 +475,11 @@ PHP_FUNCTION(stream_wrapper_register) uwrap->ce = ce; uwrap->wrapper.wops = &user_stream_wops; uwrap->wrapper.abstract = uwrap; - uwrap->wrapper.is_url = ((flags & PHP_STREAM_IS_URL) != 0); + if ((flags & PHP_STREAM_IS_URL) != 0) { + uwrap->wrapper.is_url = STREAM_IS_URL_ALWAYS; + } else { + uwrap->wrapper.is_url = STREAM_IS_URL_NEVER; + } rsrc = zend_register_resource(uwrap, le_protocols);