]> git.ipfire.org Git - thirdparty/postgresql.git/commitdiff
Prevent path traversal in pg_basebackup and pg_rewind
authorMichael Paquier <michael@paquier.xyz>
Mon, 11 May 2026 12:13:50 +0000 (05:13 -0700)
committerNoah Misch <noah@leadboat.com>
Mon, 11 May 2026 12:13:50 +0000 (05:13 -0700)
pg_rewind and pg_basebackup could be fed paths from rogue endpoints that
could overwrite the contents of the client when received, achieving path
traversal.

There were two areas in the tree that were sensitive to this problem:
- pg_basebackup, through the astreamer code, where no validation was
performed before building an output path when streaming tar data.  This
is an issue in v15 and newer versions.
- pg_rewind file operations for paths received through libpq, for all
the stable branches supported.

In order to address this problem, this commit adds a helper function in
path.c, that reuses path_is_relative_and_below_cwd() after applying
canonicalize_path().  This can be used to validate the paths received
from a connection point.  A path is considered invalid if any of the two
following conditions is satisfied:
- The path is absolute.
- The path includes a direct parent-directory reference.

Reported-by: XlabAI Team of Tencent Xuanwu Lab
Reported-by: Valery Gubanov <valerygubanov95@gmail.com>
Author: Michael Paquier <michael@paquier.xyz>
Reviewed-by: Amit Kapila <amit.kapila16@gmail.com>
Backpatch-through: 14
Security: CVE-2026-6475

src/bin/pg_basebackup/bbstreamer_file.c
src/bin/pg_basebackup/bbstreamer_tar.c
src/bin/pg_rewind/file_ops.c
src/include/port.h
src/port/path.c

index 265e80214af3aa1e41b8e1415082b1e546eab9cb..54417837ff2c5d99af9493bc199ce2286a1d528e 100644 (file)
@@ -215,6 +215,10 @@ bbstreamer_extractor_content(bbstreamer *streamer, bbstreamer_member *member,
                case BBSTREAMER_MEMBER_HEADER:
                        Assert(mystreamer->file == NULL);
 
+                       if (!path_is_safe_for_extraction(member->pathname))
+                               pg_fatal("tar member has unsafe path name: \"%s\"",
+                                                member->pathname);
+
                        /* Prepend basepath. */
                        snprintf(mystreamer->filename, sizeof(mystreamer->filename),
                                         "%s/%s", mystreamer->basepath, member->pathname);
@@ -233,6 +237,14 @@ bbstreamer_extractor_content(bbstreamer *streamer, bbstreamer_member *member,
 
                                if (mystreamer->link_map)
                                        linktarget = mystreamer->link_map(linktarget);
+
+                               if (!is_absolute_path(linktarget) &&
+                                       !path_is_safe_for_extraction(member->linktarget))
+                               {
+                                       pg_fatal("link target has unsafe path name: \"%s\"",
+                                                        member->linktarget);
+                               }
+
                                extract_link(mystreamer->filename, linktarget);
                        }
                        else
index 8c28e1c8e2ac342f6ab8aa8eb74d7a9eecf4e0f7..838d285681fd29145a930bd7ef47aa4bea050f1b 100644 (file)
@@ -295,6 +295,9 @@ bbstreamer_tar_header(bbstreamer_tar_parser *mystreamer)
        strlcpy(member->pathname, &buffer[0], MAXPGPATH);
        if (member->pathname[0] == '\0')
                pg_fatal("tar member has empty name");
+       if (!path_is_safe_for_extraction(member->pathname))
+               pg_fatal("tar member has unsafe path name: \"%s\"",
+                                member->pathname);
        member->size = read_tar_number(&buffer[124], 12);
        member->mode = read_tar_number(&buffer[100], 8);
        member->uid = read_tar_number(&buffer[108], 8);
index f88f6872f4bf4e63d740fed62293707763e1af14..f741d2a822791d74f76dab11c9b22ed3da08b44b 100644 (file)
@@ -48,6 +48,9 @@ open_target_file(const char *path, bool trunc)
 {
        int                     mode;
 
+       if (!path_is_safe_for_extraction(path))
+               pg_fatal("target file path is unsafe for open: \"%s\"", path);
+
        if (dry_run)
                return;
 
@@ -188,6 +191,9 @@ remove_target_file(const char *path, bool missing_ok)
 {
        char            dstpath[MAXPGPATH];
 
+       if (!path_is_safe_for_extraction(path))
+               pg_fatal("target file path is unsafe for removal: \"%s\"", path);
+
        if (dry_run)
                return;
 
@@ -208,6 +214,9 @@ truncate_target_file(const char *path, off_t newsize)
        char            dstpath[MAXPGPATH];
        int                     fd;
 
+       if (!path_is_safe_for_extraction(path))
+               pg_fatal("target file path is unsafe for truncation: \"%s\"", path);
+
        if (dry_run)
                return;
 
@@ -230,6 +239,10 @@ create_target_dir(const char *path)
 {
        char            dstpath[MAXPGPATH];
 
+       if (!path_is_safe_for_extraction(path))
+               pg_fatal("target directory path is unsafe for directory creation: \"%s\"",
+                                path);
+
        if (dry_run)
                return;
 
@@ -244,6 +257,10 @@ remove_target_dir(const char *path)
 {
        char            dstpath[MAXPGPATH];
 
+       if (!path_is_safe_for_extraction(path))
+               pg_fatal("target directory path is unsafe for directory removal: \"%s\"",
+                                path);
+
        if (dry_run)
                return;
 
@@ -258,6 +275,9 @@ create_target_symlink(const char *path, const char *link)
 {
        char            dstpath[MAXPGPATH];
 
+       if (!path_is_safe_for_extraction(path))
+               pg_fatal("target symlink path is unsafe for creation: \"%s\"", path);
+
        if (dry_run)
                return;
 
@@ -272,6 +292,9 @@ remove_target_symlink(const char *path)
 {
        char            dstpath[MAXPGPATH];
 
+       if (!path_is_safe_for_extraction(path))
+               pg_fatal("target symlink path is unsafe for removal: \"%s\"", path);
+
        if (dry_run)
                return;
 
index 65dfa54982c153b5b4ddf3e18dc0577d3ab9185d..a3c789ae29b5ead6269d082a02bca369c70ab6d9 100644 (file)
@@ -58,6 +58,7 @@ extern void make_native_path(char *path);
 extern void cleanup_path(char *path);
 extern bool path_contains_parent_reference(const char *path);
 extern bool path_is_relative_and_below_cwd(const char *path);
+extern bool path_is_safe_for_extraction(const char *path);
 extern bool path_is_prefix_of_path(const char *path1, const char *path2);
 extern char *make_absolute_path(const char *path);
 extern const char *get_progname(const char *argv0);
index 87395cd5c532d72a38e4be1a7f273afd4b5f996b..b384f5a3f87e074521f21d4977cb7049c728b772 100644 (file)
@@ -626,6 +626,23 @@ path_is_relative_and_below_cwd(const char *path)
                return true;
 }
 
+/*
+ * Detect whether a path is safe for use during archive extraction.
+ *
+ * This applies canonicalize_path(), then it checks that the path does
+ * not contain any parent directory references.
+ */
+bool
+path_is_safe_for_extraction(const char *path)
+{
+       char            buf[MAXPGPATH];
+
+       strlcpy(buf, path, sizeof(buf));
+       canonicalize_path(buf);
+
+       return path_is_relative_and_below_cwd(buf);
+}
+
 /*
  * Detect whether path1 is a prefix of path2 (including equality).
  *