From: Daniel Stenberg Date: Thu, 24 Jul 2025 16:36:28 +0000 (+0200) Subject: urlapi: allow more path characters "raw" when asked to URL encode X-Git-Url: http://git.ipfire.org/?a=commitdiff_plain;h=refs%2Fpull%2F18024%2Fhead;p=thirdparty%2Fcurl.git urlapi: allow more path characters "raw" when asked to URL encode Setting the path component to contain the letters: ! $ & ' ( ) { } [ ] * + , ; = : @ now leaves them un-encoded when CURLU_URLENCODE is used. Amended test 1560 to verify. Reported-by: Jeroen Ooms Fixes #17977 --- diff --git a/docs/libcurl/curl_url_set.md b/docs/libcurl/curl_url_set.md index 5758a105b1..2fa31123f1 100644 --- a/docs/libcurl/curl_url_set.md +++ b/docs/libcurl/curl_url_set.md @@ -184,8 +184,10 @@ course cannot know if the provided scheme is a valid one or not. When set, curl_url_set(3) URL encodes the part on entry, except for **scheme**, **port** and **URL**. -When setting the path component with URL encoding enabled, the slash character -is skipped. +When setting the path component with URL encoding enabled, the following +characters are left as-is if present: + + ! $ & ' ( ) { } [ ] * + , ; = : @ The query part gets space-to-plus converted before the URL conversion is applied. diff --git a/lib/urlapi.c b/lib/urlapi.c index c266fd3ebc..fd1dc5feab 100644 --- a/lib/urlapi.c +++ b/lib/urlapi.c @@ -1757,13 +1757,26 @@ static CURLUcode urlset_clear(CURLU *u, CURLUPart what) return CURLUE_OK; } +static bool allowed_in_path(unsigned char x) +{ + switch(x) { + case '!': case '$': case '&': case '\'': + case '(': case ')': case '{': case '}': + case '[': case ']': case '*': case '+': + case ',': case ';': case '=': case ':': + case '@': case '/': + return TRUE; + } + return FALSE; +} + CURLUcode curl_url_set(CURLU *u, CURLUPart what, const char *part, unsigned int flags) { char **storep = NULL; bool urlencode = (flags & CURLU_URLENCODE) ? 1 : 0; bool plusencode = FALSE; - bool urlskipslash = FALSE; + bool pathmode = FALSE; bool leadingslash = FALSE; bool appendquery = FALSE; bool equalsencode = FALSE; @@ -1808,7 +1821,7 @@ CURLUcode curl_url_set(CURLU *u, CURLUPart what, case CURLUPART_PORT: return set_url_port(u, part); case CURLUPART_PATH: - urlskipslash = TRUE; + pathmode = TRUE; leadingslash = TRUE; /* enforce */ storep = &u->path; break; @@ -1850,7 +1863,7 @@ CURLUcode curl_url_set(CURLU *u, CURLUPart what, return CURLUE_OUT_OF_MEMORY; } else if(ISUNRESERVED(*i) || - ((*i == '/') && urlskipslash) || + (pathmode && allowed_in_path(*i)) || ((*i == '=') && equalsencode)) { if((*i == '=') && equalsencode) /* only skip the first equals sign */ diff --git a/tests/libtest/lib1560.c b/tests/libtest/lib1560.c index 26a98de286..7a6e0733e6 100644 --- a/tests/libtest/lib1560.c +++ b/tests/libtest/lib1560.c @@ -895,6 +895,10 @@ static const struct setgetcase setget_parts_list[] = { /* !checksrc! disable SPACEBEFORECOMMA 1 */ static const struct setcase set_parts_list[] = { + {"https://example.com/", + "path=one /$!$&'()*+;=:@{}[]%,", + "https://example.com/one%20/$!$&'()*+;=:@{}[]%25", + 0, CURLU_URLENCODE, CURLUE_OK, CURLUE_OK}, {NULL, /* start fresh! */ "scheme=https,path=/,url=\"\",", /* incomplete url, redirect to "" */ "https://example.com/",