commit e2641771bea38c1ff5bf9429eecd693932d179fe
parent 5bc71cffc1e2f51a325ee31f2110adbdbc8b00e6
Author: Chris Bracken <chris@bracken.jp>
Date: Wed, 23 Jul 2025 16:05:21 -0700
Validate paths don't escape the repo
One could imagine a contributor to a git repo submitting a patch with a
maliciously-crafted path designed to escape the repo being read -- e.g.
`some/path/../../../../../../etc/passwd` -- and cause a target file to
be emitted to the static output document.
We now validate repo paths prior to reading from them, and bail out on
absolute paths, and paths containing `..`.
Diffstat:
5 files changed, 55 insertions(+), 0 deletions(-)
diff --git a/utils.c b/utils.c
@@ -98,3 +98,30 @@ void checkfileerror(FILE* fp, const char* name, char mode) {
errx(1, "write error: %s", name);
}
}
+
+bool is_safe_repo_path(const char* path) {
+ if (path[0] == '/') {
+ return false;
+ }
+
+ const char* p = path;
+ while (*p) {
+ /* Locate the next path component, or end of path. */
+ const char* start = p;
+ while (*p && *p != '/') {
+ p++;
+ }
+
+ /* Check for "..". */
+ size_t len = p - start;
+ if (len == 2 && start[0] == '.' && start[1] == '.') {
+ return false;
+ }
+
+ /* Skip over the path delimiter. */
+ if (*p) {
+ p++;
+ }
+ }
+ return true;
+}
diff --git a/utils.h b/utils.h
@@ -1,6 +1,7 @@
#ifndef GITOUT_UTILS_H_
#define GITOUT_UTILS_H_
+#include <stdbool.h>
#include <stdio.h>
/* Concatenates path parts to "p1/p2". Exits on failure or truncation. */
@@ -27,4 +28,7 @@ FILE* efopen(const char* filename, const char* flags);
/* Exits with error if the specified FILE* has an error. */
void checkfileerror(FILE* fp, const char* name, char mode);
+/* Validates that a path is safe to use. Returns true if safe. */
+bool is_safe_repo_path(const char* path);
+
#endif // GITOUT_UTILS_H_
diff --git a/utils_tests.c b/utils_tests.c
@@ -25,3 +25,21 @@ UTEST(path_concat, CanConcatenatePathsSecondEmpty) {
EXPECT_STREQ("p1", out);
EXPECT_STREQ("p1", returned);
}
+
+UTEST(is_safe_repo_path, AllowsValidRelativePaths) {
+ EXPECT_TRUE(is_safe_repo_path("foo"));
+ EXPECT_TRUE(is_safe_repo_path("foo/bar"));
+ EXPECT_TRUE(is_safe_repo_path("foo/bar.txt"));
+ EXPECT_TRUE(is_safe_repo_path(".foo"));
+ EXPECT_TRUE(is_safe_repo_path("foo.bar/baz"));
+}
+
+UTEST(is_safe_repo_path, RejectsInvalidPaths) {
+ EXPECT_FALSE(is_safe_repo_path("/foo"));
+ EXPECT_FALSE(is_safe_repo_path("/foo/bar"));
+ EXPECT_FALSE(is_safe_repo_path(".."));
+ EXPECT_FALSE(is_safe_repo_path("../foo"));
+ EXPECT_FALSE(is_safe_repo_path("foo/.."));
+ EXPECT_FALSE(is_safe_repo_path("foo/../bar"));
+ EXPECT_FALSE(is_safe_repo_path("foo/./../bar"));
+}
diff --git a/writer/gopher/fileblob.c b/writer/gopher/fileblob.c
@@ -19,6 +19,9 @@ struct GopherFileBlob {
};
GopherFileBlob* gopher_fileblob_create(const GitRepo* repo, const char* path) {
+ if (!is_safe_repo_path(path)) {
+ errx(1, "unsafe path: %s", path);
+ }
GopherFileBlob* blob = ecalloc(1, sizeof(GopherFileBlob));
blob->repo = repo;
diff --git a/writer/html/fileblob.c b/writer/html/fileblob.c
@@ -19,6 +19,9 @@ struct HtmlFileBlob {
};
HtmlFileBlob* html_fileblob_create(const GitRepo* repo, const char* path) {
+ if (!is_safe_repo_path(path)) {
+ errx(1, "unsafe path: %s", path);
+ }
HtmlFileBlob* blob = ecalloc(1, sizeof(HtmlFileBlob));
blob->repo = repo;