[php-src] Issue #22035: CURLOPT_SSLENGINE loads arbitrary shared objects
[email protected] (thomas-chauchefoin-tob)
| Newsgroups | php.bugs |
|---|---|
| Message-ID | <[email protected]> |
Issue: https://github.com/php/php-src/issues/22035
Author: thomas-chauchefoin-tob
### Description
`ext/curl` extension passes `CURLOPT_SSLENGINE` directly to `libcurl`. When the OpenSSL backend is used, `libcurl` forwards the value to OpenSSL's dynamic engine loader, ultimately calling `dlopen()` on the path. This allows executing native code from PHP code subject to `open_basedir` / `disable_functions (which are not security boundaries anyway).
This `libcurl` option is documented in https://curl.se/libcurl/c/CURLOPT_SSLENGINE.html. When `libcurl` is built with OpenSSL, this calls `ENGINE_by_id`:
```c
static CURLcode ossl_set_engine(struct Curl_easy *data, const char *name)
{
#ifdef USE_OPENSSL_ENGINE
CURLcode result = CURLE_SSL_ENGINE_NOTFOUND;
ENGINE *e = ENGINE_by_id(name);
// (...)
}
```
https://github.com/curl/curl/blob/6f1dfab6a29242525cdc7b48f5ee49269b9cc316/lib/vtls/openssl.c#L1657-L1693
Internally, if OpenSSL can't find the engine in the default path, it loads the `dynamic` engine support and uses control commands to load `id`:
```c
if (strcmp(id, "dynamic")) {
if ((load_dir = ossl_safe_getenv("OPENSSL_ENGINES")) == NULL)
load_dir = ossl_get_enginesdir();
iterator = ENGINE_by_id("dynamic");
if (!iterator || !ENGINE_ctrl_cmd_string(iterator, "ID", id, 0) || !ENGINE_ctrl_cmd_string(iterator, "DIR_LOAD", "2", 0) || !ENGINE_ctrl_cmd_string(iterator, "DIR_ADD", load_dir, 0) || !ENGINE_ctrl_cmd_string(iterator, "LIST_ADD", "1", 0) || !ENGINE_ctrl_cmd_string(iterator, "LOAD", NULL, 0))
goto notfound;
return iterator;
}
```
https://github.com/openssl/openssl/blob/fe686e15d84334b284f883118ed92f64b409b3aa/crypto/engine/eng_list.c#L460-L466
Then the dynamic loader uses `DSO_load` with the absolute path as the second parameter `merge`:
```c
char *merge = DSO_merge(ctx->dynamic_dso, ctx->DYNAMIC_LIBNAME, s);
if (!merge)
return 0;
if (DSO_load(ctx->dynamic_dso, merge, NULL, 0)) {
```
https://github.com/openssl/openssl/blob/fe686e15d84334b284f883118ed92f64b409b3aa/crypto/engine/eng_dyn.c#L379-L382
Internally, `DSO_load` calls `dlfcn_load`, which in turn calls `dlopen` and loads the library:
```c
static int dlfcn_load(DSO *dso)
{
// (...)
char *filename = DSO_convert_filename(dso, NULL);
// (...)
ptr = dlopen(filename, flags);
```
https://github.com/openssl/openssl/blob/fe686e15d84334b284f883118ed92f64b409b3aa/crypto/dso/dso_dlfcn.c#L93-L113
I have a patch that prevents any path separator in this option, but that may be too restrictive? I'll open a PR later this week if not.
### PHP Version
```plain
PHP 8.6.0-dev (cli) (built: May 10 2026 23:58:07) (NTS)
Copyright © The PHP Group and Contributors
Zend Engine v4.6.0-dev, Copyright © Zend by Perforce
with Zend OPcache v8.6.0-dev, Copyright ©, by Zend by Perforce
```
### Operating System
_No response_