CVE-2022-25299 (published 2022-02-18T13:15Z)

This affects the package cesanta/mongoose before 7.6. The unsafe handling of file names during upload using mg_http_upload() method may enable attackers to write files to arbitrary locations outside the designated target folder.

Severity: MEDIUM
CVSS v2 base score: 5.0 (AV:N/AC:L/Au:N/C:N/I:P/A:N)
CVSS v3 base score: 7.5, HIGH (CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:H/A:N)

Weakness information:
cwe_id: CWE-552
cwe_name: Files or Directories Accessible to External Parties
description: The product makes files or directories accessible to unauthorized actors, even though they should not be.
extended_description: 
	   Web servers, FTP servers, and similar servers may store a set of files underneath a "root" directory that is accessible to the server's users.  Applications may store sensitive files underneath this root without also using access control to limit which users may request those files, if any.  Alternately, an application might package multiple files or directories into an archive file (e.g., ZIP or tar), but the application might not exclude sensitive files that are underneath those directories.
	   In cloud technologies and containers, this weakness might present itself in the form of misconfigured storage accounts that can be read or written by a public or anonymous user.
	 
         

Fix information:
repo_url: https://github.com/cesanta/mongoose
commit_details: repo_url: https://github.com/cesanta/mongoose
author: Sergey Lyubka
author_date: 2022-01-17 12:08:23+00:00
msg: Protect against the directory traversal in mg_upload()
repository: repo_url: https://github.com/cesanta/mongoose
repo_name: cesanta/mongoose
description: Embedded Web Server
repo_language: C
owner: cesanta
file_changes: filename: Makefile
change_type: ModificationType.MODIFY
diff: @@ -59,7 +59,7 @@ fuzz: fuzzer
 	$(RUN) ./fuzzer
 
 # make CC=/usr/local/opt/llvm\@8/bin/clang ASAN_OPTIONS=detect_leaks=1
-test: CFLAGS += -DMG_ENABLE_IPV6=$(IPV6) -fsanitize=address#,undefined
+test: CFLAGS += -DMG_ENABLE_IPV6=$(IPV6) -fsanitize=address,undefined
 test: mongoose.h  Makefile $(SRCS)
 	$(CC) $(SRCS) $(CFLAGS) -coverage $(LDFLAGS) -g -o unit_test
 	ASAN_OPTIONS=$(ASAN_OPTIONS) $(RUN) ./unit_test

programming_language: Shell
filename: mongoose.c
change_type: ModificationType.MODIFY
diff: @@ -1142,34 +1142,6 @@ char *mg_http_etag(char *buf, size_t len, size_t size, time_t mtime) {
   return buf;
 }
 
-#if MG_ENABLE_FILE
-int mg_http_upload(struct mg_connection *c, struct mg_http_message *hm,
-                   const char *dir) {
-  char offset[40] = "", name[200] = "", path[256];
-  mg_http_get_var(&hm->query, "offset", offset, sizeof(offset));
-  mg_http_get_var(&hm->query, "name", name, sizeof(name));
-  if (name[0] == '\0') {
-    mg_http_reply(c, 400, "", "%s", "name required");
-    return -1;
-  } else {
-    FILE *fp;
-    size_t oft = strtoul(offset, NULL, 0);
-    snprintf(path, sizeof(path), "%s%c%s", dir, MG_DIRSEP, name);
-    LOG(LL_DEBUG,
-        ("%p %d bytes @ %d [%s]", c->fd, (int) hm->body.len, (int) oft, name));
-    if ((fp = fopen(path, oft == 0 ? "wb" : "ab")) == NULL) {
-      mg_http_reply(c, 400, "", "fopen(%s): %d", name, errno);
-      return -2;
-    } else {
-      fwrite(hm->body.ptr, 1, hm->body.len, fp);
-      fclose(fp);
-      mg_http_reply(c, 200, "", "");
-      return (int) hm->body.len;
-    }
-  }
-}
-#endif
-
 static void static_cb(struct mg_connection *c, int ev, void *ev_data,
                       void *fn_data) {
   if (ev == MG_EV_WRITE || ev == MG_EV_POLL) {
@@ -1680,6 +1652,34 @@ void mg_http_delete_chunk(struct mg_connection *c, struct mg_http_message *hm) {
   c->recv.len -= ch.len;
 }
 
+#if MG_ENABLE_FILE
+int mg_http_upload(struct mg_connection *c, struct mg_http_message *hm,
+                   const char *dir) {
+  char offset[40] = "", name[200] = "", path[256];
+  mg_http_get_var(&hm->query, "offset", offset, sizeof(offset));
+  mg_http_get_var(&hm->query, "name", name, sizeof(name));
+  if (name[0] == '\0') {
+    mg_http_reply(c, 400, "", "%s", "name required");
+    return -1;
+  } else {
+    FILE *fp;
+    long oft = strtol(offset, NULL, 0);
+    snprintf(path, sizeof(path), "%s%c%s", dir, MG_DIRSEP, name);
+    remove_double_dots(path);
+    LOG(LL_DEBUG, ("%d bytes @ %ld [%s]", (int) hm->body.len, oft, path));
+    if ((fp = fopen(path, oft == 0 ? "wb" : "ab")) == NULL) {
+      mg_http_reply(c, 400, "", "fopen(%s): %d", path, errno);
+      return -2;
+    } else {
+      fwrite(hm->body.ptr, 1, hm->body.len, fp);
+      fclose(fp);
+      mg_http_reply(c, 200, "", "");
+      return (int) hm->body.len;
+    }
+  }
+}
+#endif
+
 static void http_cb(struct mg_connection *c, int ev, void *evd, void *fnd) {
   if (ev == MG_EV_READ || ev == MG_EV_CLOSE) {
     struct mg_http_message hm;

programming_language: C
filename: http.c
change_type: ModificationType.MODIFY
diff: @@ -378,34 +378,6 @@ char *mg_http_etag(char *buf, size_t len, size_t size, time_t mtime) {
   return buf;
 }
 
-#if MG_ENABLE_FILE
-int mg_http_upload(struct mg_connection *c, struct mg_http_message *hm,
-                   const char *dir) {
-  char offset[40] = "", name[200] = "", path[256];
-  mg_http_get_var(&hm->query, "offset", offset, sizeof(offset));
-  mg_http_get_var(&hm->query, "name", name, sizeof(name));
-  if (name[0] == '\0') {
-    mg_http_reply(c, 400, "", "%s", "name required");
-    return -1;
-  } else {
-    FILE *fp;
-    size_t oft = strtoul(offset, NULL, 0);
-    snprintf(path, sizeof(path), "%s%c%s", dir, MG_DIRSEP, name);
-    LOG(LL_DEBUG,
-        ("%p %d bytes @ %d [%s]", c->fd, (int) hm->body.len, (int) oft, name));
-    if ((fp = fopen(path, oft == 0 ? "wb" : "ab")) == NULL) {
-      mg_http_reply(c, 400, "", "fopen(%s): %d", name, errno);
-      return -2;
-    } else {
-      fwrite(hm->body.ptr, 1, hm->body.len, fp);
-      fclose(fp);
-      mg_http_reply(c, 200, "", "");
-      return (int) hm->body.len;
-    }
-  }
-}
-#endif
-
 static void static_cb(struct mg_connection *c, int ev, void *ev_data,
                       void *fn_data) {
   if (ev == MG_EV_WRITE || ev == MG_EV_POLL) {
@@ -916,6 +888,34 @@ void mg_http_delete_chunk(struct mg_connection *c, struct mg_http_message *hm) {
   c->recv.len -= ch.len;
 }
 
+#if MG_ENABLE_FILE
+int mg_http_upload(struct mg_connection *c, struct mg_http_message *hm,
+                   const char *dir) {
+  char offset[40] = "", name[200] = "", path[256];
+  mg_http_get_var(&hm->query, "offset", offset, sizeof(offset));
+  mg_http_get_var(&hm->query, "name", name, sizeof(name));
+  if (name[0] == '\0') {
+    mg_http_reply(c, 400, "", "%s", "name required");
+    return -1;
+  } else {
+    FILE *fp;
+    long oft = strtol(offset, NULL, 0);
+    snprintf(path, sizeof(path), "%s%c%s", dir, MG_DIRSEP, name);
+    remove_double_dots(path);
+    LOG(LL_DEBUG, ("%d bytes @ %ld [%s]", (int) hm->body.len, oft, path));
+    if ((fp = fopen(path, oft == 0 ? "wb" : "ab")) == NULL) {
+      mg_http_reply(c, 400, "", "fopen(%s): %d", path, errno);
+      return -2;
+    } else {
+      fwrite(hm->body.ptr, 1, hm->body.len, fp);
+      fclose(fp);
+      mg_http_reply(c, 200, "", "");
+      return (int) hm->body.len;
+    }
+  }
+}
+#endif
+
 static void http_cb(struct mg_connection *c, int ev, void *evd, void *fnd) {
   if (ev == MG_EV_READ || ev == MG_EV_CLOSE) {
     struct mg_http_message hm;

programming_language: C
filename: unit_test.c
change_type: ModificationType.MODIFY
diff: @@ -679,7 +679,6 @@ static void test_http_server(void) {
     char *p;
     remove("uploaded.txt");
     ASSERT((p = mg_file_read("uploaded.txt", NULL)) == NULL);
-
     ASSERT(fetch(&mgr, buf, url,
                  "POST /upload HTTP/1.0\n"
                  "Content-Length: 1\n\nx") == 400);
@@ -698,6 +697,21 @@ static void test_http_server(void) {
     remove("uploaded.txt");
   }
 
+  {
+    // Test upload directory traversal
+    char *p;
+    remove("uploaded.txt");
+    ASSERT((p = mg_file_read("uploaded.txt", NULL)) == NULL);
+    ASSERT(fetch(&mgr, buf, url,
+                 "POST /upload?name=../uploaded.txt HTTP/1.0\r\n"
+                 "Content-Length: 5\r\n"
+                 "\r\nhello") == 200);
+    ASSERT((p = mg_file_read("uploaded.txt", NULL)) != NULL);
+    ASSERT(strcmp(p, "hello") == 0);
+    free(p);
+    remove("uploaded.txt");
+  }
+
   // HEAD request
   ASSERT(fetch(&mgr, buf, url, "GET /a.txt HTTP/1.0\n\n") == 200);
   ASSERT(fetch(&mgr, buf, url, "HEAD /a.txt HTTP/1.0\n\n") == 200);

programming_language: C