Validate serialized sub pathnames in content files to prevent path traversal

This commit is contained in:
Andrea Mazzoleni 2026-08-26 22:42:08 +02:00
parent 95ac8d7b51
commit 4aef405259
4 changed files with 123 additions and 12 deletions

View file

@ -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 {

View file

@ -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 */
}

View file

@ -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 */

View file

@ -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 */