CVE-2022-25297 (published 2022-02-21T08:15Z)

This affects the package drogonframework/drogon before 1.7.5. The unsafe handling of file names during upload using HttpFile::save() method may enable attackers to write files to arbitrary locations outside the designated target folder.

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

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/drogonframework/drogon
commit_details: repo_url: https://github.com/drogonframework/drogon
author: Kirill
author_date: 2022-02-11 17:54:42+02:00
msg: Prevent malformed upload path causing arbitrary write (#1174)
repository: repo_url: https://github.com/drogonframework/drogon
repo_name: drogonframework/drogon
description: Drogon: A C++14/17/20 based HTTP web application framework running on Linux/macOS/Unix/Windows
repo_language: C++
owner: drogonframework
file_changes: filename: HttpFileImpl.cc
change_type: ModificationType.MODIFY
diff: @@ -18,6 +18,7 @@
 #include <drogon/MultiPart.h>
 #include <fstream>
 #include <iostream>
+#include <algorithm>
 
 using namespace drogon;
 
@@ -31,28 +32,45 @@ int HttpFileImpl::save(const std::string &path) const
     assert(!path.empty());
     if (fileName_.empty())
         return -1;
-    filesystem::path fsPath(utils::toNativePath(path));
-    if (!fsPath.is_absolute() &&
-        (!fsPath.has_parent_path() ||
-         (fsPath.begin()->string() != "." && fsPath.begin()->string() != "..")))
+    filesystem::path fsUploadDir(utils::toNativePath(path));
+
+    if (!fsUploadDir.is_absolute() && (!fsUploadDir.has_parent_path() ||
+                                       (fsUploadDir.begin()->string() != "." &&
+                                        fsUploadDir.begin()->string() != "..")))
     {
-        filesystem::path fsUploadPath(utils::toNativePath(
-            HttpAppFrameworkImpl::instance().getUploadPath()));
-        fsPath = fsUploadPath / fsPath;
+        fsUploadDir = utils::toNativePath(
+                          HttpAppFrameworkImpl::instance().getUploadPath()) /
+                      fsUploadDir;
     }
-    filesystem::path fsFileName(utils::toNativePath(fileName_));
-    if (!filesystem::exists(fsPath))
+
+    fsUploadDir = filesystem::weakly_canonical(fsUploadDir);
+
+    if (!filesystem::exists(fsUploadDir))
     {
-        LOG_TRACE << "create path:" << fsPath;
+        LOG_TRACE << "create path:" << fsUploadDir;
         drogon::error_code err;
-        filesystem::create_directories(fsPath, err);
+        filesystem::create_directories(fsUploadDir, err);
         if (err)
         {
             LOG_SYSERR;
             return -1;
         }
     }
-    return saveTo(fsPath / fsFileName);
+
+    filesystem::path fsSaveToPath(filesystem::weakly_canonical(
+        fsUploadDir / utils::toNativePath(fileName_)));
+
+    if (!std::equal(fsUploadDir.begin(),
+                    fsUploadDir.end(),
+                    fsSaveToPath.begin()))
+    {
+        LOG_ERROR
+            << "Attempt writing outside of upload directory detected. Path: "
+            << fileName_;
+        return -1;
+    }
+
+    return saveTo(fsSaveToPath);
 }
 int HttpFileImpl::saveAs(const std::string &fileName) const
 {

programming_language: C++
filename: CMakeLists.txt
change_type: ModificationType.MODIFY
diff: @@ -1,45 +1,49 @@
 link_libraries(${PROJECT_NAME})
-set(UNITTEST_SOURCES unittests/main.cc
-                        unittests/Base64Test.cc
-                        unittests/UrlCodecTest.cc
-                        unittests/GzipTest.cc
-                        unittests/HttpViewDataTest.cc
-                        unittests/CookieTest.cc
-                        unittests/ClassNameTest.cc
-                        unittests/HttpDateTest.cc
-                        unittests/HttpHeaderTest.cc
-                        unittests/MD5Test.cc
-                        unittests/MsgBufferTest.cc
-                        unittests/OStringStreamTest.cc
-                        unittests/PubSubServiceUnittest.cc
-                        unittests/Sha1Test.cc
-                        ../src/ssl_funcs/Sha1.cc
-                        ../src/HttpUtils.cc
-                        unittests/FileTypeTest.cc
-                        unittests/DrObjectTest.cc
-                        unittests/HttpFullDateTest.cc
-                        unittests/MainLoopTest.cc
-                        unittests/CacheMapTest.cc
-                        unittests/StringOpsTest.cc)
+set(UNITTEST_SOURCES
+    unittests/main.cc
+    unittests/Base64Test.cc
+    unittests/UrlCodecTest.cc
+    unittests/GzipTest.cc
+    unittests/HttpViewDataTest.cc
+    unittests/CookieTest.cc
+    unittests/ClassNameTest.cc
+    unittests/HttpDateTest.cc
+    unittests/HttpHeaderTest.cc
+    unittests/MD5Test.cc
+    unittests/MsgBufferTest.cc
+    unittests/OStringStreamTest.cc
+    unittests/PubSubServiceUnittest.cc
+    unittests/Sha1Test.cc
+    ../src/ssl_funcs/Sha1.cc
+    unittests/FileTypeTest.cc
+    unittests/DrObjectTest.cc
+    unittests/HttpFullDateTest.cc
+    unittests/MainLoopTest.cc
+    unittests/CacheMapTest.cc
+    unittests/StringOpsTest.cc)
 
 if(DROGON_CXX_STANDARD GREATER_EQUAL 20 AND HAS_COROUTINE)
-    set(UNITTEST_SOURCES ${UNITTEST_SOURCES} unittests/CoroutineTest.cc)
+  set(UNITTEST_SOURCES ${UNITTEST_SOURCES} unittests/CoroutineTest.cc)
 endif()
 
 if(Brotli_FOUND)
-    set(UNITTEST_SOURCES ${UNITTEST_SOURCES} unittests/BrotliTest.cc)
+  set(UNITTEST_SOURCES ${UNITTEST_SOURCES} unittests/BrotliTest.cc)
 endif()
 
-if (CMAKE_CXX_COMPILER_ID MATCHES "MSVC" AND BUILD_DROGON_SHARED)
-    set(UNITTEST_SOURCES ${UNITTEST_SOURCES} ../src/HttpUtils.cc)
+if(CMAKE_CXX_COMPILER_ID MATCHES "MSVC" AND BUILD_DROGON_SHARED)
+  set(UNITTEST_SOURCES ${UNITTEST_SOURCES} ../src/HttpUtils.cc)
+else()
+  set(UNITTEST_SOURCES ${UNITTEST_SOURCES} ../src/HttpFileImpl.cc
+                       unittests/HttpFileTest.cc)
 endif()
 
 add_executable(unittest ${UNITTEST_SOURCES})
 
-set(INTEGRATION_TEST_CLIENT_SOURCES integration_test/client/main.cc
-                                    integration_test/client/WebSocketTest.cc
-                                    integration_test/client/MultipleWsTest.cc
-                                    integration_test/client/HttpPipeliningTest.cc)
+set(INTEGRATION_TEST_CLIENT_SOURCES
+    integration_test/client/main.cc
+    integration_test/client/WebSocketTest.cc
+    integration_test/client/MultipleWsTest.cc
+    integration_test/client/HttpPipeliningTest.cc)
 add_executable(integration_test_client ${INTEGRATION_TEST_CLIENT_SOURCES})
 
 set(INTEGRATION_TEST_SERVER_SOURCES
@@ -64,10 +68,11 @@ set(INTEGRATION_TEST_SERVER_SOURCES
     integration_test/server/main.cc)
 
 if(DROGON_CXX_STANDARD GREATER_EQUAL 20 AND HAS_COROUTINE)
-    set(INTEGRATION_TEST_SERVER_SOURCES ${INTEGRATION_TEST_SERVER_SOURCES}
-        integration_test/server/api_v1_CoroTest.cc)
-    set(CMAKE_CXX_STANDARD 20)
-    set(CMAKE_CXX_STANDARD_REQUIRED TRUE)
+  set(INTEGRATION_TEST_SERVER_SOURCES
+      ${INTEGRATION_TEST_SERVER_SOURCES}
+      integration_test/server/api_v1_CoroTest.cc)
+  set(CMAKE_CXX_STANDARD 20)
+  set(CMAKE_CXX_STANDARD_REQUIRED TRUE)
 endif(DROGON_CXX_STANDARD GREATER_EQUAL 20 AND HAS_COROUTINE)
 
 add_executable(integration_test_server ${INTEGRATION_TEST_SERVER_SOURCES})
@@ -98,9 +103,8 @@ add_custom_command(
           $<TARGET_FILE_DIR:integration_test_server>/a-directory)
 
 set(tests unittest integration_test_server integration_test_client)
-set_property(TARGET ${tests}
-             PROPERTY CXX_STANDARD ${DROGON_CXX_STANDARD})
+set_property(TARGET ${tests} PROPERTY CXX_STANDARD ${DROGON_CXX_STANDARD})
 set_property(TARGET ${tests} PROPERTY CXX_STANDARD_REQUIRED ON)
 set_property(TARGET ${tests} PROPERTY CXX_EXTENSIONS OFF)
 
-ParseAndAddDrogonTests(unittest)
+parseandadddrogontests(unittest)

programming_language: Shell
filename: HttpFileTest.cc
change_type: ModificationType.ADD
diff: @@ -0,0 +1,52 @@
+#include "../../lib/src/HttpFileImpl.h"
+#include <drogon/drogon_test.h>
+#include <filesystem>
+
+using namespace drogon;
+using namespace std;
+
+DROGON_TEST(HttpFile)
+{
+    SUBSECTION(Save)
+    {
+        HttpFileImpl file;
+        file.setFileName("test_file_name");
+        file.setFile("test", 4);
+        auto out = file.save("./test_uploads_dir");
+
+        CHECK(out == 0);
+        CHECK(filesystem::exists("./test_uploads_dir/test_file_name"));
+
+        filesystem::remove_all("./test_uploads_dir");
+    }
+
+    SUBSECTION(SavePathRelativeTraversal)
+    {
+        auto uploadPath = filesystem::current_path() / "test_uploads_dir";
+
+        HttpFileImpl file;
+        file.setFileName("../test_malicious_file_name");
+        file.setFile("test", 4);
+        auto out = file.save(uploadPath.string());
+
+        CHECK(out == -1);
+        CHECK(!filesystem::exists(uploadPath / "../test_malicious_file_name"));
+
+        filesystem::remove_all(uploadPath);
+        filesystem::remove(uploadPath / "../test_malicious_file_name");
+    }
+
+    SUBSECTION(SavePathAbsoluteTraversal)
+    {
+        HttpFileImpl file;
+        file.setFileName("/tmp/test_malicious_file_name");
+        file.setFile("test", 4);
+        auto out = file.save("./test_uploads_dir");
+
+        CHECK(out == -1);
+        CHECK(!filesystem::exists("/tmp/test_malicious_file_name"));
+
+        filesystem::remove_all("test_uploads_dir");
+        filesystem::remove_all("/tmp/test_malicious_file_name");
+    }
+}

programming_language: Perl