From 4aef405259325e34050147f45423ddd4f9c85679 Mon Sep 17 00:00:00 2001 From: Andrea Mazzoleni Date: Wed, 26 Aug 2026 22:42:08 +0200 Subject: [PATCH] Validate serialized sub pathnames in content files to prevent path traversal --- cmdline/selftest.c | 51 ++++++++++++++++++++++++++++++++++++++++++++++ cmdline/state.c | 24 +++++++++++----------- cmdline/support.c | 48 +++++++++++++++++++++++++++++++++++++++++++ cmdline/support.h | 12 +++++++++++ 4 files changed, 123 insertions(+), 12 deletions(-) diff --git a/cmdline/selftest.c b/cmdline/selftest.c index 317f1462..d8722005 100644 --- a/cmdline/selftest.c +++ b/cmdline/selftest.c @@ -1027,6 +1027,57 @@ static void test_path(void) /* LCOV_EXCL_STOP */ } #endif + + static const struct { + const char* path; + int expected; + } sub_tests[] = { + { "", 0 }, + { "/", 0 }, + { "/a", 0 }, + { "a/", 0 }, + { "a//b", 0 }, + { ".", 0 }, + { "..", 0 }, + { "./a", 0 }, + { "../a", 0 }, + { "a/.", 0 }, + { "a/..", 0 }, + { "a/./b", 0 }, + { "a/../b", 0 }, +#ifdef _WIN32 + { "\\a", 0 }, + { "a\\b", 0 }, + { "C:a", 0 }, + { "C:/a", 0 }, + { "d:file.txt", 0 }, +#else + { "\\a", 1 }, + { "a\\b", 1 }, + { "C:a", 1 }, + { "C:/a", 1 }, + { "d:file.txt", 1 }, +#endif + { "a", 1 }, + { "file.txt", 1 }, + { "dir/file.txt", 1 }, + { "a/b/c/d.txt", 1 }, + { ".hidden", 1 }, + { "dir/.hidden", 1 }, + { ".../file", 1 }, + { "..file", 1 }, + { "file..", 1 }, + { "a/b:c/d", 1 }, + }; + for (unsigned i = 0; i < sizeof(sub_tests) / sizeof(sub_tests[0]); ++i) { + if (path_is_sub(sub_tests[i].path) != sub_tests[i].expected) { + /* LCOV_EXCL_START */ + log_fatal(EINTERNAL, "Failed path_is_sub test for '%s', expected %d\n", + sub_tests[i].path, sub_tests[i].expected); + exit(EXIT_FAILURE); + /* LCOV_EXCL_STOP */ + } + } } struct { diff --git a/cmdline/state.c b/cmdline/state.c index b64bd3ff..4fbdd1f5 100644 --- a/cmdline/state.c +++ b/cmdline/state.c @@ -2398,10 +2398,10 @@ static void state_read_content(struct snapraid_state* state, const char* path, S os_abort(); /* LCOV_EXCL_STOP */ } - if (!*sub) { + if (!path_is_sub(sub)) { /* LCOV_EXCL_START */ decoding_error(path, f); - log_fatal(EINTERNAL, "Internal inconsistency: Null file!\n"); + log_fatal(EINTERNAL, "Internal inconsistency: Invalid file '%s'!\n", sub); os_abort(); /* LCOV_EXCL_STOP */ } @@ -2774,10 +2774,10 @@ static void state_read_content(struct snapraid_state* state, const char* path, S os_abort(); /* LCOV_EXCL_STOP */ } - if (!*sub) { + if (!path_is_sub(sub)) { /* LCOV_EXCL_START */ decoding_error(path, f); - log_fatal(EINTERNAL, "Internal inconsistency: Null dealloc!\n"); + log_fatal(EINTERNAL, "Internal inconsistency: Invalid dealloc '%s'!\n", sub); os_abort(); /* LCOV_EXCL_STOP */ } @@ -2868,10 +2868,10 @@ static void state_read_content(struct snapraid_state* state, const char* path, S /* LCOV_EXCL_STOP */ } - if (!*sub) { + if (!path_is_sub(sub)) { /* LCOV_EXCL_START */ decoding_error(path, f); - log_fatal(EINTERNAL, "Internal inconsistency: Null symlink!\n"); + log_fatal(EINTERNAL, "Internal inconsistency: Invalid symlink '%s'!\n", sub); os_abort(); /* LCOV_EXCL_STOP */ } @@ -2919,10 +2919,10 @@ static void state_read_content(struct snapraid_state* state, const char* path, S /* LCOV_EXCL_STOP */ } - if (!*sub) { + if (!path_is_sub(sub)) { /* LCOV_EXCL_START */ decoding_error(path, f); - log_fatal(EINTERNAL, "Internal inconsistency: Null hardlink!\n"); + log_fatal(EINTERNAL, "Internal inconsistency: Invalid hardlink '%s'!\n", sub); os_abort(); /* LCOV_EXCL_STOP */ } @@ -2935,10 +2935,10 @@ static void state_read_content(struct snapraid_state* state, const char* path, S /* LCOV_EXCL_STOP */ } - if (!*linkto) { + if (!path_is_sub(linkto)) { /* LCOV_EXCL_START */ decoding_error(path, f); - log_fatal(EINTERNAL, "Internal inconsistency: Empty hardlink '%s'!\n", sub); + log_fatal(EINTERNAL, "Internal inconsistency: Invalid hardlink target '%s' for '%s'!\n", linkto, sub); os_abort(); /* LCOV_EXCL_STOP */ } @@ -2977,10 +2977,10 @@ static void state_read_content(struct snapraid_state* state, const char* path, S /* LCOV_EXCL_STOP */ } - if (!*sub) { + if (!path_is_sub(sub)) { /* LCOV_EXCL_START */ decoding_error(path, f); - log_fatal(EINTERNAL, "Internal inconsistency: Null dir!\n"); + log_fatal(EINTERNAL, "Internal inconsistency: Invalid dir '%s'!\n", sub); os_abort(); /* LCOV_EXCL_STOP */ } diff --git a/cmdline/support.c b/cmdline/support.c index b728d615..3eb7f103 100644 --- a/cmdline/support.c +++ b/cmdline/support.c @@ -1267,6 +1267,54 @@ int path_is_root_of(const char* root, const char* path) return *root == 0 && *path != 0; } +int path_is_sub(const char* sub) +{ + const char* p; + size_t len; + +#ifdef _WIN32 + /* cannot start with a Windows drive letter like C: */ + if (isalpha((unsigned char)sub[0]) && sub[1] == ':') + return 0; +#endif + + len = 0; + p = sub; + while (1) { + char c = *p; + + if (c == '/' || c == 0) { + /* empty component (leading, consecutive, trailing slash, or empty string) */ + if (len == 0) + return 0; + + /* dot component */ + if (len == 1 && *(p - 1) == '.') + return 0; + + /* dot-dot component */ + if (len == 2 && *(p - 2) == '.' && *(p - 1) == '.') + return 0; + + if (c == 0) + break; + + len = 0; +#ifdef _WIN32 + } else if (c == '\\') { + /* cannot contain backslash in Windows */ + return 0; +#endif + } else { + ++len; + } + + ++p; + } + + return 1; +} + /****************************************************************************/ /* file-system */ diff --git a/cmdline/support.h b/cmdline/support.h index 0867df9d..5c70eda5 100644 --- a/cmdline/support.h +++ b/cmdline/support.h @@ -369,6 +369,18 @@ int pathncmp(const char* a, const char* b, size_t n); */ int path_is_root_of(const char* root, const char* path); +/** + * Check if the path is a canonical relative sub-path. + * + * A valid sub-path must: + * - Not be empty + * - Not start or end with a slash '/' + * - Not contain backslashes '\\' (on Windows) + * - Not start with a drive letter (e.g., "C:" on Windows) + * - Not contain empty components ("//"), "." or ".." + */ +int path_is_sub(const char* sub); + /****************************************************************************/ /* file-system */