]> git.ipfire.org Git - thirdparty/curl.git/commitdiff
urlapi: make dedotdotify handle leading dots correctly
authorDaniel Stenberg <daniel@haxx.se>
Wed, 18 Mar 2026 08:14:59 +0000 (09:14 +0100)
committerDaniel Stenberg <daniel@haxx.se>
Wed, 18 Mar 2026 10:14:20 +0000 (11:14 +0100)
Paths starting with one or two leading dots but without a following
slash were not handled correctly.

Follow-up to c31dd6631f9a0177aa9045cdbb

Extended test 1395 accordingly with a set of new test string.

Reported by Codex Security

Closes #20974

lib/urlapi.c
tests/unit/unit1395.c

index e49291f41b12a4c079213f7877d5469dc2b644bc..e9262b42fc3c51233df45d591baa9aeb3d5e8d78 100644 (file)
@@ -718,8 +718,12 @@ UNITTEST int dedotdotify(const char *input, size_t clen, char **outp)
   struct dynbuf out;
   CURLcode result = CURLE_OK;
 
+  /* variables for leading dot checks */
+  const char *dinput = input;
+  size_t dlen = clen;
+
   *outp = NULL;
-  /* the path always starts with a slash, and a slash has not dot */
+  /* a single byte path cannot be cleaned up */
   if(clen < 2)
     return 0;
 
@@ -727,9 +731,9 @@ UNITTEST int dedotdotify(const char *input, size_t clen, char **outp)
 
   /*  A. If the input buffer begins with a prefix of "../" or "./", then
       remove that prefix from the input buffer; otherwise, */
-  if(is_dot(&input, &clen)) {
-    const char *p = input;
-    size_t blen = clen;
+  if(is_dot(&dinput, &dlen)) {
+    const char *p = dinput;
+    size_t blen = dlen;
 
     if(!clen)
       /* . [end] */
@@ -737,7 +741,7 @@ UNITTEST int dedotdotify(const char *input, size_t clen, char **outp)
     else if(ISSLASH(*p)) {
       /* one dot followed by a slash */
       input = p + 1;
-      clen--;
+      clen = dlen - 1;
     }
 
     /*  D. if the input buffer consists only of "." or "..", then remove
index 7ca7f22c105c6214ddc6018fe9fc36c207092558..b80f9927d623dc9a6483998a86fe7d6bf808ac0a 100644 (file)
@@ -37,6 +37,21 @@ static CURLcode test_unit1395(const char *arg)
   };
 
   const struct dotdot pairs[] = {
+    { "/%2f%2e%2e%2f/../a", "/a" },
+    { "/%2f%2e%2e%2f/../", "/" },
+    { "/%2f%2e%2e%2f/.", "/%2f%2e%2e%2f/" },
+    { "/%2f%2e%2e%2f/", "/%2f%2e%2e%2f/" },
+    { "/%2f%2e%2e%2f", "/%2f%2e%2e%2f" },
+    { "/%2f%2e%2e%2", "/%2f%2e%2e%2" },
+    { "/%2f%2e%2e%", "/%2f%2e%2e%" },
+    { "/%2f%2e%2e", "/%2f%2e%2e" },
+    { "/%2f%2e%2", "/%2f%2e%2" },
+    { "/%2f%2e%", "/%2f%2e%" },
+    { "/%2f%2e", "/%2f%2e" },
+    { "/%2f%2", "/%2f%2" },
+    { "/%2f%", "/%2f%" },
+    { "/%2f", "/%2f" },
+    { "/%2", "/%2" },
     { "%2f%2e%2e%2f/../a", "%2f%2e%2e%2f/a" },
     { "%2f%2e%2e%2f/../", "%2f%2e%2e%2f/" },
     { "%2f%2e%2e%2f/.", "%2f%2e%2e%2f/" },
@@ -108,6 +123,26 @@ static CURLcode test_unit1395(const char *arg)
     { "/moo/..", "/" },
     { "/..", "/" },
     { "/.", "/" },
+    { "////../a", "///a" },
+    { "/../../../../../../", "/" },
+    { "/..//..//", "//" },
+    { "/.config/../ssh", "/ssh" },
+    { "/..config/..", "/" },
+    { "/.../a", "/.../a" },
+    { "/a/%2E%2e/b", "/b" },
+    { "/a/%2e./b", "/b" },
+    { "/a/.%2e/b", "/b" },
+    { "/%2f..%2f", "/%2f..%2f" },
+    { "/a/b/.", "/a/b/" },
+    { "/a/b/..", "/a/" },
+    { "well-known", "well-known" },
+    { ".well-known", ".well-known" },
+    { "..well-known", "..well-known" },
+    { "...well-known", "...well-known" },
+    { "....well-known", "....well-known" },
+    { "%2ewell-known", "%2ewell-known" },
+    { "%2Ewell-known", "%2Ewell-known" },
+    { "../.well-known", ".well-known" },
   };
 
   for(i = 0; i < CURL_ARRAYSIZE(pairs); i++) {