]> git.ipfire.org Git - thirdparty/dovecot/core.git/commitdiff
lib-dict-extra: Escape paths in username for private dict keys
authorKarl Fleischmann <karl.fleischmann@open-xchange.com>
Wed, 18 Mar 2026 13:58:50 +0000 (14:58 +0100)
committeraki.tuomi <aki.tuomi@open-xchange.com>
Fri, 20 Mar 2026 08:23:10 +0000 (08:23 +0000)
Prevent path traversal issues in username when doing dict lookups with
private dict keys.

src/lib-dict-extra/dict-fs.c
src/lib-dict-extra/test-dict-fs.c

index ad761e1b12bd3132f2d20fdab765e4a38ec0ea69..5a3c78d0fa342a46efcea15d19977d370d7e828f 100644 (file)
@@ -57,6 +57,13 @@ static const char *fs_dict_escape_key(const char *key)
 {
        const char *ptr;
        string_t *new_key = NULL;
+       /* A key always starts with either "priv/" or "shared/". Usernames can
+          start with "./" or "../". Thus make sure the given key has no such
+          prefix. */
+       if (str_begins_with(key, "./") || str_begins_with(key, "../")) {
+               new_key = t_str_new(strlen(key));
+               str_append(new_key, "..");
+       }
        /* we take the slow path always if we see potential
           need for escaping */
        while ((ptr = strstr(key, "/.")) != NULL) {
@@ -86,7 +93,7 @@ static const char *fs_dict_get_full_key(const char *username, const char *key)
        if (str_begins(key, DICT_PATH_SHARED, &key))
                return key;
        else if (str_begins(key, DICT_PATH_PRIVATE, &key))
-               return t_strdup_printf("%s/%s", username, key);
+               return t_strdup_printf("%s/%s", fs_dict_escape_key(username), key);
        else
                i_unreached();
 }
index fb79a40eb225cf525554c3e42fd4e3676c3f7e3d..0e5a3772b9d3e6177ee117ef956de752e89206e6 100644 (file)
@@ -94,6 +94,32 @@ static void test_dict_fs_set_get(void)
        test_assert(test_file_exists(".test-dict/.test"));
        test_dict_set_get(dict, "testuser", "shared/..test", "6");
        test_assert(test_file_exists(".test-dict/..test"));
+
+       /* make sure path traversal in usernames is prevented */
+       const char *username_test_key = "priv/./key";
+       struct {
+               const char *username;
+               const char *path;
+       } username_test_cases[] = {
+               { ".testuser", ".test-dict/.testuser/...key" },
+               { "./testuser",   ".test-dict/.../testuser/.../key" },
+               { "./.testuser",   ".test-dict/.../.testuser/.../key" },
+               { "../testuser", ".test-dict/..../testuser/.../key" },
+               { "../.testuser", ".test-dict/..../.testuser/.../key" },
+               { "././testuser", ".test-dict/.../.../testuser/.../key" },
+               { "././.testuser", ".test-dict/.../.../testuser/.../key" },
+               { "./../testuser", ".test-dict/.../..../testuser/.../key" },
+               { "./../.testuser", ".test-dict/.../..../testuser/.../key" },
+               { ".././testuser", ".test-dict/..../.../testuser/.../key" },
+               { "./../.testuser", ".test-dict/.../..../testuser/.../key" },
+               { "../../testuser", ".test-dict/..../..../testuser/.../key" },
+               { "../../.testuser", ".test-dict/..../..../testuser/.../key" },
+       };
+       for (size_t i = 0; i < N_ELEMENTS(username_test_cases); i++) {
+               test_dict_set_get(dict, username_test_cases[i].username,
+                                 username_test_key, "1");
+               test_assert(test_file_exists(test_cases[i].path));
+       }
        dict_deinit(&dict);
 
        if (unlink_directory(".test-dict", UNLINK_DIRECTORY_FLAG_RMDIR, &error) < 0)