diff --git a/pghoard/webserver.py b/pghoard/webserver.py index f92cee28..261839ea 100644 --- a/pghoard/webserver.py +++ b/pghoard/webserver.py @@ -427,11 +427,18 @@ def _try_save_and_verify_restored_file(self, filetype, filename, prefetch_target os.unlink(prefetch_target_path) return e + def _validate_target_path(self, site, filename): + xlog_file = Path(os.path.relpath(filename)) + xlog_dir = Path(get_pg_wal_directory(self.server.config["backup_sites"][site])) + if xlog_dir not in xlog_file.parents: + raise HttpResponse("Invalid xlog file path {!r}".format(filename), status=400) + def get_wal_or_timeline_file(self, site, filename, filetype): target_path = self.headers.get("x-pghoard-target-path") if not target_path: raise HttpResponse("x-pghoard-target-path header missing from download", status=400) + self._validate_target_path(site, target_path) self._process_completed_download_operations() # See if we have already prefetched the file diff --git a/test/test_webserver.py b/test/test_webserver.py index 4a9ff725..16a29737 100644 --- a/test/test_webserver.py +++ b/test/test_webserver.py @@ -737,3 +737,17 @@ def test_parse_request_invalid_path(self, pghoard): resp = conn.getresponse() assert resp.status == 400 assert resp.read() == b"Invalid 'archive' request, only single file retrieval is supported for now" + + def test_uncontrolled_target_path(self, pghoard, tmpdir): + wal_seg = "0000000100000001000000AB" + wal_file = "/{}/archive/{}".format(pghoard.test_site, wal_seg) + conn = HTTPConnection(host="127.0.0.1", port=pghoard.config["http_port"]) + headers = {"x-pghoard-target-path": "/etc/passwd"} + conn.request("GET", wal_file, headers=headers) + status = conn.getresponse().status + assert status == 400 + + headers = {"x-pghoard-target-path": "/root/.ssh/id_rsa"} + conn.request("GET", wal_file, headers=headers) + status = conn.getresponse().status + assert status == 400