@@ -964,6 +964,96 @@ LT_BEGIN_AUTO_TEST(file_upload_suite, file_upload_with_content_type)
964964 ws->stop ();
965965LT_END_AUTO_TEST (file_upload_with_content_type)
966966
967+ // Send file with a crafted filename for path traversal testing
968+ static std::pair<CURLcode, int32_t> send_file_with_traversal_name(int port, const char * crafted_filename) {
969+ curl_global_init (CURL_GLOBAL_ALL);
970+
971+ CURL *curl = curl_easy_init ();
972+
973+ curl_mime *form = curl_mime_init (curl);
974+ curl_mimepart *field = curl_mime_addpart (form);
975+ curl_mime_name (field, TEST_KEY);
976+ // Use the real file for data, but override the filename
977+ curl_mime_filedata (field, TEST_CONTENT_FILEPATH);
978+ curl_mime_filename (field, crafted_filename);
979+
980+ CURLcode res;
981+ std::string url = " localhost:" + std::to_string (port) + " /upload" ;
982+ curl_easy_setopt (curl, CURLOPT_URL, url.c_str ());
983+ curl_easy_setopt (curl, CURLOPT_MIMEPOST, form);
984+
985+ res = curl_easy_perform (curl);
986+ long http_code = 0 ; // NOLINT [runtime/int]
987+ curl_easy_getinfo (curl, CURLINFO_RESPONSE_CODE, &http_code);
988+
989+ curl_easy_cleanup (curl);
990+ curl_mime_free (form);
991+ return {res, http_code};
992+ }
993+
994+ // Test that path traversal filenames are rejected
995+ LT_BEGIN_AUTO_TEST (file_upload_suite, file_upload_path_traversal_rejected)
996+ string upload_directory = "upload_test_dir";
997+ MKDIR (upload_directory.c_str());
998+
999+ int port = PORT + 2 ;
1000+ auto ws = std::make_unique<webserver>(create_webserver(port)
1001+ .file_upload_target(httpserver::FILE_UPLOAD_DISK_ONLY)
1002+ .file_upload_dir(upload_directory));
1003+ // NOT using generate_random_filename_on_upload - this is the vulnerable path
1004+ ws->start (false );
1005+ LT_CHECK_EQ (ws->is_running (), true);
1006+
1007+ print_file_upload_resource resource;
1008+ LT_ASSERT_EQ (true , ws->register_resource (" upload" , &resource));
1009+
1010+ // Attempt path traversal with "../escape"
1011+ send_file_with_traversal_name (port, " ../escape" );
1012+ // The server should reject the upload (MHD_NO causes connection close)
1013+ // The key check is that no file was created outside the upload dir
1014+ LT_CHECK_EQ (file_exists(" escape" ), false);
1015+ LT_CHECK_EQ (file_exists(" ./escape" ), false);
1016+
1017+ ws->stop ();
1018+
1019+ // Clean up
1020+ rmdir (upload_directory.c_str());
1021+ LT_END_AUTO_TEST (file_upload_path_traversal_rejected)
1022+
1023+ // Test that sanitize keeps the basename for normal filenames
1024+ LT_BEGIN_AUTO_TEST(file_upload_suite, file_upload_sanitize_keeps_basename)
1025+ string upload_directory = "upload_test_dir";
1026+ MKDIR (upload_directory.c_str());
1027+
1028+ int port = PORT + 3 ;
1029+ auto ws = std::make_unique<webserver>(create_webserver(port)
1030+ .file_upload_target(httpserver::FILE_UPLOAD_DISK_ONLY)
1031+ .file_upload_dir(upload_directory));
1032+ ws->start (false );
1033+ LT_CHECK_EQ (ws->is_running (), true);
1034+
1035+ print_file_upload_resource resource;
1036+ LT_ASSERT_EQ (true , ws->register_resource (" upload" , &resource));
1037+
1038+ // Upload with a path-like filename — should strip to just "myfile.txt"
1039+ auto res = send_file_with_traversal_name(port, " some/path/myfile.txt" );
1040+ LT_ASSERT_EQ (res.first, 0 );
1041+ LT_ASSERT_EQ (res.second, 201 );
1042+
1043+ // The file should be created with only the basename
1044+ map<string, map<string, httpserver::http::file_info>> files = resource.get_files();
1045+ LT_CHECK_EQ (files.size(), 1);
1046+ auto file = files.begin()->second.begin();
1047+ string expected_path = upload_directory + " /myfile.txt" ;
1048+ LT_CHECK_EQ (file->second.get_file_system_file_name(), expected_path);
1049+
1050+ ws->stop ();
1051+
1052+ // Clean up
1053+ unlink (expected_path.c_str());
1054+ rmdir (upload_directory.c_str());
1055+ LT_END_AUTO_TEST (file_upload_sanitize_keeps_basename)
1056+
9671057LT_BEGIN_AUTO_TEST_ENV()
9681058 AUTORUN_TESTS()
9691059LT_END_AUTO_TEST_ENV()
0 commit comments