new file mode 100644
@@ -0,0 +1,302 @@
+From 63f45b182c53fa98727f37d6ef9ff7e5b4d1ed06 Mon Sep 17 00:00:00 2001
+From: Daniel Stenberg <daniel@haxx.se>
+Date: Wed, 17 Apr 2024 11:39:25 +0200
+Subject: [PATCH] urlapi: add CURLU_GET_EMPTY for empty queries and fragments
+MIME-Version: 1.0
+Content-Type: text/plain; charset=UTF-8
+Content-Transfer-Encoding: 8bit
+
+By default the API inhibits empty queries and fragments extracted.
+Unless this new flag is set.
+
+This also makes the behavior more consistent: without it set, zero
+length queries and fragments are considered not present in the URL. With
+the flag set, they are returned as a zero length strings if they were in
+fact present in the URL.
+
+This applies when extracting the individual query and fragment
+components and for the full URL.
+
+Closes #13396
+
+Upstream-Status: Backport [https://github.com/curl/curl/commit/3eac21d86bc50ba459a9a8a0f3d1130b6d30b57d]
+
+Signed-off-by: João Marcos Costa <joaomarcos.costa@bootlin.com>
+---
+ docs/libcurl/curl_url_get.md | 12 ++++++++++
+ docs/libcurl/symbols-in-versions | 1 +
+ include/curl/urlapi.h | 3 +++
+ lib/urlapi.c | 37 +++++++++++++++++++++-------
+ tests/data/test1201 | 4 ++--
+ tests/libtest/lib1560.c | 41 ++++++++++++++++++++++++++++++++
+ 6 files changed, 88 insertions(+), 10 deletions(-)
+
+diff --git a/docs/libcurl/curl_url_get.md b/docs/libcurl/curl_url_get.md
+index 2f3b89b5e5..100eea7f52 100644
+--- a/docs/libcurl/curl_url_get.md
++++ b/docs/libcurl/curl_url_get.md
+@@ -113,6 +113,18 @@ punycode.
+
+ (Added in curl 8.3.0)
+
++## CURLU_GET_EMPTY
++
++When this flag is used in curl_url_get(), it makes the function return empty
++query and fragments parts or when used in the full URL. By default, libcurl
++otherwise considers empty parts non-existing.
++
++An empty query part is one where this is nothing following the question mark
++(before the possible fragment). An empty fragments part is one where there is
++nothing following the hash sign.
++
++(Added in curl 8.8.0)
++
+ # PARTS
+
+ ## CURLUPART_URL
+diff --git a/docs/libcurl/symbols-in-versions b/docs/libcurl/symbols-in-versions
+index a627dd68b6..2a9a0ff6f5 100644
+--- a/docs/libcurl/symbols-in-versions
++++ b/docs/libcurl/symbols-in-versions
+@@ -1062,6 +1062,7 @@ CURLU_APPENDQUERY 7.62.0
+ CURLU_DEFAULT_PORT 7.62.0
+ CURLU_DEFAULT_SCHEME 7.62.0
+ CURLU_DISALLOW_USER 7.62.0
++CURLU_GET_EMPTY 8.8.0
+ CURLU_GUESS_SCHEME 7.62.0
+ CURLU_NO_AUTHORITY 7.67.0
+ CURLU_NO_DEFAULT_PORT 7.62.0
+diff --git a/include/curl/urlapi.h b/include/curl/urlapi.h
+index 91f8c4548a..19388c3c01 100644
+--- a/include/curl/urlapi.h
++++ b/include/curl/urlapi.h
+@@ -99,6 +99,9 @@ typedef enum {
+ #define CURLU_ALLOW_SPACE (1<<11) /* Allow spaces in the URL */
+ #define CURLU_PUNYCODE (1<<12) /* get the host name in punycode */
+ #define CURLU_PUNY2IDN (1<<13) /* punycode => IDN conversion */
++#define CURLU_GET_EMPTY (1<<14) /* allow empty queries and fragments
++ when extracting the URL or the
++ components */
+
+ typedef struct Curl_URL CURLU;
+
+diff --git a/lib/urlapi.c b/lib/urlapi.c
+index dc42489722..a7abd0b248 100644
+--- a/lib/urlapi.c
++++ b/lib/urlapi.c
+@@ -79,7 +79,9 @@ struct Curl_URL {
+ char *path;
+ char *query;
+ char *fragment;
+- long portnum; /* the numerical version */
++ unsigned short portnum; /* the numerical version */
++ BIT(query_present); /* to support blank */
++ BIT(fragment_present); /* to support blank */
+ };
+
+ #define DEFAULT_SCHEME "https"
+@@ -558,7 +560,7 @@ UNITTEST CURLUcode Curl_parse_port(struct Curl_URL *u, struct dynbuf *host,
+ if(rest[0])
+ return CURLUE_BAD_PORT_NUMBER;
+
+- u->portnum = port;
++ u->portnum = (unsigned short) port;
+ /* generate a new port number string to get rid of leading zeroes etc */
+ free(u->port);
+ u->port = aprintf("%ld", port);
+@@ -1242,6 +1244,7 @@ static CURLUcode parseurl(const char *url, CURLU *u, unsigned int flags)
+ fragment = strchr(path, '#');
+ if(fragment) {
+ fraglen = pathlen - (fragment - path);
++ u->fragment_present = TRUE;
+ if(fraglen > 1) {
+ /* skip the leading '#' in the copy but include the terminating null */
+ if(flags & CURLU_URLENCODE) {
+@@ -1269,6 +1272,7 @@ static CURLUcode parseurl(const char *url, CURLU *u, unsigned int flags)
+ size_t qlen = fragment ? (size_t)(fragment - query) :
+ pathlen - (query - path);
+ pathlen -= qlen;
++ u->query_present = TRUE;
+ if(qlen > 1) {
+ if(flags & CURLU_URLENCODE) {
+ struct dynbuf enc;
+@@ -1404,6 +1408,8 @@ CURLU *curl_url_dup(const CURLU *in)
+ DUP(u, in, fragment);
+ DUP(u, in, zoneid);
+ u->portnum = in->portnum;
++ u->fragment_present = in->fragment_present;
++ u->query_present = in->query_present;
+ }
+ return u;
+ fail:
+@@ -1488,10 +1494,16 @@ CURLUcode curl_url_get(const CURLU *u, CURLUPart what,
+ ptr = u->query;
+ ifmissing = CURLUE_NO_QUERY;
+ plusdecode = urldecode;
++ if(ptr && !ptr[0] && !(flags & CURLU_GET_EMPTY))
++ /* there was a blank query and the user do not ask for it */
++ ptr = NULL;
+ break;
+ case CURLUPART_FRAGMENT:
+ ptr = u->fragment;
+ ifmissing = CURLUE_NO_FRAGMENT;
++ if(!ptr && u->fragment_present && flags & CURLU_GET_EMPTY)
++ /* there was a blank fragment and the user asks for it */
++ ptr = "";
+ break;
+ case CURLUPART_URL: {
+ char *url;
+@@ -1499,13 +1511,18 @@ CURLUcode curl_url_get(const CURLU *u, CURLUPart what,
+ char *options = u->options;
+ char *port = u->port;
+ char *allochost = NULL;
++ bool show_fragment =
++ u->fragment || (u->fragment_present && flags & CURLU_GET_EMPTY);
++ bool show_query =
++ (u->query && u->query[0]) ||
++ (u->query_present && flags & CURLU_GET_EMPTY);
+ punycode = (flags & CURLU_PUNYCODE)?1:0;
+ depunyfy = (flags & CURLU_PUNY2IDN)?1:0;
+ if(u->scheme && strcasecompare("file", u->scheme)) {
+ url = aprintf("file://%s%s%s",
+ u->path,
+- u->fragment? "#": "",
+- u->fragment? u->fragment : "");
++ show_fragment ? "#": "",
++ u->fragment ? u->fragment : "");
+ }
+ else if(!u->host)
+ return CURLUE_NO_HOST;
+@@ -1593,9 +1610,9 @@ CURLUcode curl_url_get(const CURLU *u, CURLUPart what,
+ port ? ":": "",
+ port ? port : "",
+ u->path ? u->path : "/",
+- (u->query && u->query[0]) ? "?": "",
+- (u->query && u->query[0]) ? u->query : "",
+- u->fragment? "#": "",
++ show_query ? "?": "",
++ u->query ? u->query : "",
++ show_fragment ? "#": "",
+ u->fragment? u->fragment : "");
+ free(allochost);
+ }
+@@ -1730,9 +1747,11 @@ CURLUcode curl_url_set(CURLU *u, CURLUPart what,
+ break;
+ case CURLUPART_QUERY:
+ storep = &u->query;
++ u->query_present = FALSE;
+ break;
+ case CURLUPART_FRAGMENT:
+ storep = &u->fragment;
++ u->fragment_present = FALSE;
+ break;
+ default:
+ return CURLUE_UNKNOWN_PART;
+@@ -1816,9 +1835,11 @@ CURLUcode curl_url_set(CURLU *u, CURLUPart what,
+ appendquery = (flags & CURLU_APPENDQUERY)?1:0;
+ equalsencode = appendquery;
+ storep = &u->query;
++ u->query_present = TRUE;
+ break;
+ case CURLUPART_FRAGMENT:
+ storep = &u->fragment;
++ u->fragment_present = TRUE;
+ break;
+ case CURLUPART_URL: {
+ /*
+@@ -1969,6 +1990,6 @@ nomem:
+ /* set after the string, to make it not assigned if the allocation above
+ fails */
+ if(port)
+- u->portnum = port;
++ u->portnum = (unsigned short)port;
+ return CURLUE_OK;
+ }
+diff --git a/tests/data/test1201 b/tests/data/test1201
+index 296e7fa6a0..f4c1a48386 100644
+--- a/tests/data/test1201
++++ b/tests/data/test1201
+@@ -25,7 +25,7 @@ gopher
+ Gopher selector
+ </name>
+ <command>
+-gopher://%HOSTIP:%GOPHERPORT/1/selector/SELECTOR/%TESTNUMBER?
++gopher://%HOSTIP:%GOPHERPORT/1/selector/SELECTOR/%TESTNUMBER
+ </command>
+ </client>
+
+@@ -33,7 +33,7 @@ gopher://%HOSTIP:%GOPHERPORT/1/selector/SELECTOR/%TESTNUMBER?
+ # Verify data after the test has been "shot"
+ <verify>
+ <protocol>
+-/selector/SELECTOR/%TESTNUMBER?
++/selector/SELECTOR/%TESTNUMBER
+ </protocol>
+ </verify>
+ </testcase>
+diff --git a/tests/libtest/lib1560.c b/tests/libtest/lib1560.c
+index 1509c76a7f..2a26874e54 100644
+--- a/tests/libtest/lib1560.c
++++ b/tests/libtest/lib1560.c
+@@ -151,6 +151,21 @@ struct clearurlcase {
+ };
+
+ static const struct testcase get_parts_list[] ={
++ {"https://curl.se/#",
++ "https | [11] | [12] | [13] | curl.se | [15] | / | [16] | ",
++ 0, CURLU_GET_EMPTY, CURLUE_OK},
++ {"https://curl.se/?#",
++ "https | [11] | [12] | [13] | curl.se | [15] | / | | ",
++ 0, CURLU_GET_EMPTY, CURLUE_OK},
++ {"https://curl.se/?",
++ "https | [11] | [12] | [13] | curl.se | [15] | / | | [17]",
++ 0, CURLU_GET_EMPTY, CURLUE_OK},
++ {"https://curl.se/?",
++ "https | [11] | [12] | [13] | curl.se | [15] | / | [16] | [17]",
++ 0, 0, CURLUE_OK},
++ {"https://curl.se/?#",
++ "https | [11] | [12] | [13] | curl.se | [15] | / | [16] | [17]",
++ 0, 0, CURLUE_OK},
+ {"https://curl.se/# ",
+ "https | [11] | [12] | [13] | curl.se | [15] | / | [16] | %20%20",
+ CURLU_URLENCODE|CURLU_ALLOW_SPACE, 0, CURLUE_OK},
+@@ -508,6 +523,20 @@ static const struct testcase get_parts_list[] ={
+ };
+
+ static const struct urltestcase get_url_list[] = {
++ {"http://user@example.com?#",
++ "http://user@example.com/?#",
++ 0, CURLU_GET_EMPTY, CURLUE_OK},
++ /* WHATWG disgrees, it wants "https:/0.0.0.0/" */
++ {"https://0x.0x.0", "https://0x.0x.0/", 0, 0, CURLUE_OK},
++
++ {"https://example.com:000000000000000000000443/foo",
++ "https://example.com/foo",
++ 0, CURLU_NO_DEFAULT_PORT, CURLUE_OK},
++ {"https://example.com:000000000000000000000/foo",
++ "https://example.com:0/foo",
++ 0, CURLU_NO_DEFAULT_PORT, CURLUE_OK},
++ {"https://192.0x0000A80001", "https://192.168.0.1/", 0, 0, CURLUE_OK},
++ {"https://0xffffffff", "https://255.255.255.255/", 0, 0, CURLUE_OK},
+ {"https://1.0x1000000", "https://1.0x1000000/", 0, 0, CURLUE_OK},
+ {"https://0x7f.1", "https://127.0.0.1/", 0, 0, CURLUE_OK},
+ {"https://1.2.3.256.com", "https://1.2.3.256.com/", 0, 0, CURLUE_OK},
+@@ -770,6 +799,18 @@ static int checkurl(const char *org, const char *url, const char *out)
+ 3. Extract all components (not URL)
+ */
+ static const struct setgetcase setget_parts_list[] = {
++ {"https://example.com/",
++ "query=\"\",",
++ "https | [11] | [12] | [13] | example.com | [15] | / | | [17]",
++ 0, 0, CURLU_GET_EMPTY, CURLUE_OK},
++ {"https://example.com/",
++ "fragment=\"\",",
++ "https | [11] | [12] | [13] | example.com | [15] | / | [16] | ",
++ 0, 0, CURLU_GET_EMPTY, CURLUE_OK},
++ {"https://example.com/",
++ "query=\"\",",
++ "https | [11] | [12] | [13] | example.com | [15] | / | [16] | [17]",
++ 0, 0, 0, CURLUE_OK},
+ {"https://example.com",
+ "path=get,",
+ "https | [11] | [12] | [13] | example.com | [15] | /get | [16] | [17]",
new file mode 100644
@@ -0,0 +1,222 @@
+From 91b6ecabaed3a6400d5d1a86990a3fec9ccbe09c Mon Sep 17 00:00:00 2001
+From: Daniel Stenberg <daniel@haxx.se>
+Date: Mon, 21 Sep 2026 11:13:13 +0200
+Subject: [PATCH] getinfo: make sure CURLINFO_EFFECTIVE_URL does not contain
+ creds
+MIME-Version: 1.0
+Content-Type: text/plain; charset=UTF-8
+Content-Transfer-Encoding: 8bit
+
+Reported-by: Joshua Rogers
+
+Verified in test 1876
+
+Closes #23016
+
+CVE: CVE-2026-94266
+Upstream-Status: Backport [https://github.com/curl/curl/commit/7a6bd027d0de2b6857472d35da090ef60cc29f19]
+
+Signed-off-by: João Marcos Costa <joaomarcos.costa@bootlin.com>
+---
+ lib/getinfo.c | 45 +++++++++++++++++++++++++--
+ lib/transfer.c | 1 +
+ lib/url.c | 1 +
+ lib/urldata.h | 1 +
+ tests/data/Makefile.inc | 2 +-
+ tests/data/test1876 | 67 +++++++++++++++++++++++++++++++++++++++++
+ 6 files changed, 114 insertions(+), 3 deletions(-)
+ create mode 100644 tests/data/test1876
+
+diff --git a/lib/getinfo.c b/lib/getinfo.c
+index dd43643d8b..d972e081d8 100644
+--- a/lib/getinfo.c
++++ b/lib/getinfo.c
+@@ -73,6 +73,9 @@ CURLcode Curl_initinfo(struct Curl_easy *data)
+ free(info->contenttype);
+ info->contenttype = NULL;
+
++ free(info->effective_url);
++ info->effective_url= NULL;
++
+ free(info->wouldredirect);
+ info->wouldredirect = NULL;
+
+@@ -91,13 +94,51 @@ CURLcode Curl_initinfo(struct Curl_easy *data)
+ return CURLE_OK;
+ }
+
++static CURLcode effective_url(struct Curl_easy *data,
++ const char **param_charp)
++{
++ const char *s = data->state.url;
++ *param_charp = s ? s : "";
++ if(s) {
++ char *url = NULL;
++ /* Make a copy of the URL without credentials */
++ CURLU *u = curl_url();
++ CURLUcode uc;
++ if(!u)
++ return CURLE_OUT_OF_MEMORY;
++
++ uc = curl_url_set(u, CURLUPART_URL, s,
++ CURLU_NON_SUPPORT_SCHEME |
++ (data->set.path_as_is ? CURLU_PATH_AS_IS : 0));
++ if(!uc)
++ uc = curl_url_set(u, CURLUPART_USER, NULL, 0);
++ if(!uc)
++ uc = curl_url_set(u, CURLUPART_PASSWORD, NULL, 0);
++ if(!uc)
++ uc = curl_url_get(u, CURLUPART_URL, &url, CURLU_GET_EMPTY);
++
++ curl_url_cleanup(u);
++
++ if(uc || !url)
++ return CURLE_OUT_OF_MEMORY;
++ if(strcmp(url, s)) {
++ /* they are different */
++ free(data->info.effective_url);
++ data->info.effective_url = url;
++ *param_charp = url;
++ }
++ else
++ curl_free(url);
++ }
++ return CURLE_OK;
++}
++
+ static CURLcode getinfo_char(struct Curl_easy *data, CURLINFO info,
+ const char **param_charp)
+ {
+ switch(info) {
+ case CURLINFO_EFFECTIVE_URL:
+- *param_charp = data->state.url?data->state.url:(char *)"";
+- break;
++ return effective_url(data, param_charp);
+ case CURLINFO_EFFECTIVE_METHOD: {
+ const char *m = data->set.str[STRING_CUSTOMREQUEST];
+ if(!m) {
+diff --git a/lib/transfer.c b/lib/transfer.c
+index a73462928d..d4550f8184 100644
+--- a/lib/transfer.c
++++ b/lib/transfer.c
+@@ -645,6 +645,7 @@ CURLcode Curl_pretransfer(struct Curl_easy *data)
+ data->state.authhost.want = data->set.httpauth;
+ data->state.authproxy.want = data->set.proxyauth;
+ Curl_safefree(data->info.wouldredirect);
++ Curl_safefree(data->info.effective_url);
+ Curl_data_priority_clear_state(data);
+
+ if(data->state.httpreq == HTTPREQ_PUT)
+diff --git a/lib/url.c b/lib/url.c
+index 9e1ca0336c..5153406287 100644
+--- a/lib/url.c
++++ b/lib/url.c
+@@ -291,6 +291,7 @@ CURLcode Curl_close(struct Curl_easy **datap)
+ #endif
+ Curl_safefree(data->info.contenttype);
+ Curl_safefree(data->info.wouldredirect);
++ Curl_safefree(data->info.effective_url);
+
+ /* this destroys the channel and we cannot use it anymore after this */
+ Curl_resolver_cancel(data);
+diff --git a/lib/urldata.h b/lib/urldata.h
+index 4fc595a639..7c5a3daa67 100644
+--- a/lib/urldata.h
++++ b/lib/urldata.h
+@@ -1031,6 +1031,7 @@ struct PureInfo {
+ long numconnects; /* how many new connection did libcurl created */
+ char *contenttype; /* the content type of the object */
+ char *wouldredirect; /* URL this would've been redirected to if asked to */
++ char *effective_url; /* when needed */
+ curl_off_t retry_after; /* info from Retry-After: header */
+ unsigned int header_size; /* size of read header(s) in bytes */
+
+diff --git a/tests/data/Makefile.inc b/tests/data/Makefile.inc
+index 9efae9cefe..9d4f9281d0 100644
+--- a/tests/data/Makefile.inc
++++ b/tests/data/Makefile.inc
+@@ -219,7 +219,7 @@ test1680 test1681 test1682 test1683 \
+ \
+ test1700 test1701 test1702 test1703 test1704 \
+ \
+-test1800 test1801 \
++test1800 test1801 test1876 \
+ \
+ test1900 test1903 test1904 test1905 test1906 test1907 \
+ test1908 test1909 test1910 test1911 test1912 test1913 test1914 test1915 \
+diff --git a/tests/data/test1876 b/tests/data/test1876
+new file mode 100644
+index 0000000000..4fc3b9fd02
+--- /dev/null
++++ b/tests/data/test1876
+@@ -0,0 +1,67 @@
++<?xml version="1.0" encoding="US-ASCII"?>
++<testcase>
++<info>
++<keywords>
++netrc
++</keywords>
++</info>
++
++# Server-side
++<reply>
++<data crlf="headers" nocheck="yes">
++HTTP/1.1 301 redir
++Content-Length: 0
++Content-Type: text/html
++Location: %TESTNUMBER0002
++
++</data>
++
++<data2 crlf="headers">
++HTTP/1.1 200 OK
++Content-Length: 0
++Content-Type: text/html
++
++</data2>
++
++</reply>
++
++# Client-side
++<client>
++<server>
++http
++</server>
++<name>
++verify no credentials present in effective URL after redirect
++</name>
++<file name="%LOGDIR/netrc">
++machine %HOSTIP
++login netrcuser
++password netrcSECRET123
++</file>
++
++<command option="no-include">
++http://%HOSTIP:%HTTPPORT/%TESTNUMBER --netrc-optional --netrc-file %LOGDIR/netrc -L -w 'EFFECTIVE_URL=%{url_effective}\n'
++</command>
++</client>
++
++# Verify data after the test has been "shot"
++<verify>
++<protocol crlf="headers">
++GET /%TESTNUMBER HTTP/1.1
++Host: %HOSTIP:%HTTPPORT
++Authorization: Basic %b64[netrcuser:netrcSECRET123]b64%
++User-Agent: curl/%VERSION
++Accept: */*
++
++GET /%TESTNUMBER0002 HTTP/1.1
++Host: %HOSTIP:%HTTPPORT
++Authorization: Basic %b64[netrcuser:netrcSECRET123]b64%
++User-Agent: curl/%VERSION
++Accept: */*
++
++</protocol>
++<stdout>
++EFFECTIVE_URL=http://%HOSTIP:%HTTPPORT/%TESTNUMBER0002
++</stdout>
++</verify>
++</testcase>
+--
+2.55.0
+
@@ -49,6 +49,8 @@ SRC_URI = "https://curl.se/download/${BP}.tar.xz \
file://CVE-2026-92386.patch \
file://CVE-2026-92389.patch \
file://CVE-2026-92391.patch \
+ file://CVE-2026-94266-pre.patch \
+ file://CVE-2026-94266.patch \
"
SRC_URI:append:class-nativesdk = " \
This vulnerability, marked as High [1], was fixed upstream [2] since v8.23.0. Backport the fix to curl v8.7.1. The *-pre.patch includes essentially the definition of CURLU_GET_EMPTY, needed by the actual CVE fix, and the calls to curlx_free and curlx_safefree were replaced by their respective counterparts available in curl v8.7.1. Also, in effective_url(), the call to Curl_bufref_ptr() was replaced a plain const char * assignment, as curl v8.7.1 defines stat.url as char* (as opposed to struct bufref in v8.23.0). While I tried backporting the commit [3] that turns it into a bufref, it just raised too many conflicts. [1] https://nvd.nist.gov/vuln/detail/cve-2026-94266 [2] https://curl.se/docs/CVE-2026-94266.html [3] https://github.com/curl/curl/commit/36542b734966fce2ce5689d689a0e4922a103e50 Signed-off-by: João Marcos Costa (Schneider Electric) <joaomarcos.costa@bootlin.com> --- .../curl/curl/CVE-2026-94266-pre.patch | 302 ++++++++++++++++++ .../curl/curl/CVE-2026-94266.patch | 222 +++++++++++++ meta/recipes-support/curl/curl_8.7.1.bb | 2 + 3 files changed, 526 insertions(+) create mode 100644 meta/recipes-support/curl/curl/CVE-2026-94266-pre.patch create mode 100644 meta/recipes-support/curl/curl/CVE-2026-94266.patch