feat(petal): harden against untrusted input and bound work
2 files changed, -164/+170
petal/config.def.h
@@ -7,6 +7,9 @@ static const char *service = "git"; static const int loglimit = 100; static const int maxdepth = 8; static const int barwidth = 40;+static const int maxblob = 512 * 1024;+static const int maxdiff = 10000;+static const int maxcpu = 10; static const Service services[] = { { "git", "https://git.lostduk.com/" },
petal/petal.c
@@ -7,14 +7,15 @@ #include <stdlib.h> #include <string.h> #include <time.h>+#include <sys/resource.h> #include <sys/stat.h> #include <git2.h> /* macros */ #define LENGTH(X) (sizeof(X) / sizeof((X)[0]))-#define HTML "text/html; charset=utf-8" #define HOUR 3600L #define DAY (24 * HOUR)+#define MAXTIME 253402300799LL /* enums */ typedef struct {@@ -24,16 +25,16 @@ typedef struct { /* function declarations */ static int bprintf(char *buf, size_t len, const char *fmt, ...);-static int commitdiff(git_diff **diff, git_commit *commit);+static int commitdiff(git_diff **diff, git_commit *commit, int renames); static void commitpage(const char *id); static const char *envor(const char *name, const char *fallback); static void filepage(const char *path, const git_tree_entry *e); static int findref(char *spec, char **rest);-static int headcommit(git_commit **commit, git_repository *r); static void httpheader(int status, const char *type); static void indexpage(void); static void indexrow(const char *name); static int isrepo(const char *name);+static int loadcommit(const char *id); static int loadref(git_reference *r); static void logpage(void); static void logrow(const git_oid *oid);@@ -43,14 +44,13 @@ static int openrepo(git_repository **r, const char *name); static void pagebegin(int status, const char *pagetitle); static void pageend(void); static void printage(const git_time *t);-static void printdiff(git_diff *diff);+static void printdiff(git_diff *diff, git_patch **patches); static void printname(const char *name); static void printpath(const char *path);-static void printstat(git_diff *diff);+static void printstat(git_diff *diff, git_patch **patches); static void printtime(const git_time *t, const char *fmt); static void printurl(const char *page, const char *r, const char *path); static void rawpage(const char *path);-static void readdesc(const char *name, char *buf, size_t len); static void readfile(const char *name, const char *file, char *buf, size_t len); static void refrows(const char *glob);@@ -62,9 +62,8 @@ static void treerow(const git_tree_entry *e, const char *path); static void treetable(const git_tree *tree, const char *path); static void urlencode(const char *s); static int validname(const char *s);+static int validtime(const git_time *t); static void walk(const char *rel, int depth);-static void walkent(const char *dir, const char *rel, const char *e,- int depth); static void xmlencode(const char *s); static void xmlencoden(const char *s, size_t len); @@ -75,6 +74,7 @@ static const char refshead[] = static const char *base; static const char *reponame; static git_repository *repo;+static git_odb *odb; static char ref[PATH_MAX]; static git_oid tip; static long ofs;@@ -97,7 +97,7 @@ bprintf(char *buf, size_t len, const char *fmt, ...) } int-commitdiff(git_diff **diff, git_commit *commit)+commitdiff(git_diff **diff, git_commit *commit, int renames) { git_commit *parent = NULL; git_tree *tree = NULL, *ptree = NULL;@@ -122,7 +122,8 @@ commitdiff(git_diff **diff, git_commit *commit) } if ((err = git_diff_tree_to_tree(diff, repo, ptree, tree, &opts)) < 0) goto end;- err = git_diff_find_similar(*diff, &fopts);+ if (renames)+ err = git_diff_find_similar(*diff, &fopts); end: if (err < 0) { git_diff_free(*diff);@@ -137,20 +138,17 @@ end: void commitpage(const char *id) {- char spec[128];- git_object *obj; git_commit *c; git_diff *diff;+ git_patch **patches = NULL; const git_signature *a; const char *body, *pid;- unsigned int i;+ size_t i, n = 0; - if (bprintf(spec, sizeof(spec), "%s^{commit}", id) < 0- || git_revparse_single(&obj, repo, spec) < 0) {+ if (loadcommit(id) < 0 || git_commit_lookup(&c, repo, &tip) < 0) { notfound(); return; }- c = (git_commit *)obj; a = git_commit_author(c); pagebegin(200, git_commit_summary(c));@@ -177,11 +175,23 @@ commitpage(const char *id) xmlencode(a->email); fputs("></td></tr>\n<tr><td>date</td><td>", stdout); printtime(&a->when, "%Y-%m-%d %H:%M");- printf(" %c%02d%02d</td></tr>\n</table>\n", a->when.sign,- abs(a->when.offset) / 60, abs(a->when.offset) % 60);- if (commitdiff(&diff, c) >= 0) {- printstat(diff);- printdiff(diff);+ if (validtime(&a->when))+ printf(" %c%02d%02d", a->when.sign, abs(a->when.offset) / 60,+ abs(a->when.offset) % 60);+ fputs("</td></tr>\n</table>\n", stdout);+ if (commitdiff(&diff, c, 1) >= 0) {+ n = git_diff_num_deltas(diff);+ patches = calloc(n + 1, sizeof(*patches));+ }+ if (patches) {+ for (i = 0; i < n; i++)+ if (git_patch_from_diff(&patches[i], diff, i) < 0)+ patches[i] = NULL;+ printstat(diff, patches);+ printdiff(diff, patches);+ for (i = 0; i < n; i++)+ git_patch_free(patches[i]);+ free(patches); } pageend(); git_diff_free(diff);@@ -200,34 +210,38 @@ envor(const char *name, const char *fallback) void filepage(const char *path, const git_tree_entry *e) {- git_blob *blob;- const char *s, *nl;- size_t len, i, l, n, lines;- int binary;+ git_blob *blob = NULL;+ git_object_t type;+ const char *s = NULL, *nl;+ size_t len, i, l, n, lines = 0; - if (git_blob_lookup(&blob, repo, git_tree_entry_id(e)) < 0) {+ if (git_odb_read_header(&len, &type, odb, git_tree_entry_id(e)) < 0+ || (len <= (size_t)maxblob+ && git_blob_lookup(&blob, repo, git_tree_entry_id(e)) < 0)) { notfound(); return; }- s = git_blob_rawcontent(blob);- len = (size_t)git_blob_rawsize(blob);- binary = git_blob_is_binary(blob);- for (i = 0, lines = 0; i < len; i++)- if (s[i] == '\n')+ if (blob && !git_blob_is_binary(blob)) {+ s = git_blob_rawcontent(blob);+ for (i = 0; i < len; i++)+ if (s[i] == '\n')+ lines++;+ if (len && s[len - 1] != '\n') lines++;- if (len && s[len - 1] != '\n')- lines++;+ } pagebegin(200, reponame); repoheader("tree"); printpath(path); fputs("<span class=\"info\">", stdout);- if (!binary)+ if (s) printf("%zu lines · ", lines); printf("%zuB · <a href=\"", len); printurl("raw", ref, path); fputs("\">raw</a></span></p>\n", stdout);- if (binary) {+ if (!blob) {+ fputs("<p>File too large.</p>\n", stdout);+ } else if (!s) { fputs("<p>Binary file.</p>\n", stdout); } else { fputs("<pre class=\"blob\">", stdout);@@ -252,8 +266,8 @@ findref(char *spec, char **rest) char *cut = NULL, *p; for (;;) {- if (git_reference_dwim(&r, repo, spec) >= 0- && loadref(r) >= 0) {+ if ((git_reference_dwim(&r, repo, spec) >= 0+ && loadref(r) >= 0) || loadcommit(spec) >= 0) { *rest = cut ? cut + 1 : spec + strlen(spec); return 0; }@@ -269,24 +283,15 @@ findref(char *spec, char **rest) } } -int-headcommit(git_commit **commit, git_repository *r)-{- git_object *obj;-- if (git_revparse_single(&obj, r, "HEAD^{commit}") < 0)- return -1;- *commit = (git_commit *)obj;- return 0;-}- void httpheader(int status, const char *type) { if (status == 404) fputs("Status: 404 Not Found\r\n", stdout); printf("Content-Type: %s\r\n", type);- fputs("X-Content-Type-Options: nosniff\r\n\r\n", stdout);+ fputs("Content-Security-Policy: default-src 'none'; "+ "style-src 'self'; img-src 'self'\r\n"+ "X-Content-Type-Options: nosniff\r\n\r\n", stdout); } void@@ -307,9 +312,9 @@ indexrow(const char *name) { char desc[256], owner[256]; git_repository *r = NULL;- git_commit *commit = NULL;+ git_object *obj = NULL; - readdesc(name, desc, sizeof(desc));+ readfile(name, "description", desc, sizeof(desc)); readfile(name, "owner", owner, sizeof(owner)); fputs("<tr><td class=\"name\"><a href=\"", stdout); xmlencode(base);@@ -322,12 +327,12 @@ indexrow(const char *name) fputs("</td><td class=\"owner sm\">", stdout); xmlencode(owner); fputs("</td>", stdout);- if (openrepo(&r, name) >= 0 && headcommit(&commit, r) >= 0)- printage(&git_commit_committer(commit)->when);- else- fputs("<td class=\"r age a4\">—</td>", stdout);+ if (openrepo(&r, name) >= 0+ && git_revparse_single(&obj, r, "HEAD^{commit}") < 0)+ obj = NULL;+ printage(obj ? &git_commit_committer((git_commit *)obj)->when : NULL); fputs("</tr>\n", stdout);- git_commit_free(commit);+ git_object_free(obj); git_repository_free(r); } @@ -339,11 +344,28 @@ isrepo(const char *name) if (!validname(name)) return 0;- if (bprintf(path, sizeof(path), "%s/%s.git", root, name) < 0)- return 0;- if (stat(path, &st) < 0)+ if (bprintf(path, sizeof(path), "%s/%s.git/git-daemon-export-ok",+ root, name) < 0) return 0;- return S_ISDIR(st.st_mode);+ return stat(path, &st) >= 0;+}++int+loadcommit(const char *id)+{+ git_oid oid;+ git_commit *c;+ size_t len;++ len = strlen(id);+ if (len < 4 || len > GIT_OID_SHA1_HEXSIZE+ || git_oid_fromstrn(&oid, id, len) < 0+ || git_commit_lookup_prefix(&c, repo, &oid, len) < 0)+ return -1;+ git_oid_cpy(&tip, git_commit_id(c));+ git_oid_tostr(ref, sizeof(ref), &tip);+ git_commit_free(c);+ return 0; } int@@ -397,16 +419,12 @@ logpage(void) if (ofs > loglimit) printf("?ofs=%ld", ofs - loglimit); fputs("\">← newer</a>", stdout);- } else {- fputs("<span>← newer</span>", stdout); } printf("<span>%ld–%ld</span>", ofs + 1, ofs + n); if (more) { fputs("<a href=\"", stdout); printurl("log", ref, NULL); printf("?ofs=%ld\">older →</a>", ofs + n);- } else {- fputs("<span>older →</span>", stdout); } fputs("</nav>\n", stdout); }@@ -435,7 +453,7 @@ logrow(const git_oid *oid) xmlencode(git_commit_summary(commit)); fputs("</a></td><td class=\"sm\">", stdout); xmlencode(author->name);- if (commitdiff(&diff, commit) >= 0)+ if (commitdiff(&diff, commit, 0) >= 0) git_diff_get_stats(&stats, diff); if (stats) printf("</td><td class=\"r num sm\">%zu</td>"@@ -487,7 +505,7 @@ pagebegin(int status, const char *pagetitle) { size_t i; - httpheader(status, HTML);+ httpheader(status, "text/html; charset=utf-8"); fputs("<!DOCTYPE html>\n<html>\n<head>\n<meta charset=\"utf-8\">\n" "<meta name=\"viewport\" " "content=\"width=device-width, initial-scale=1\">\n<title>",@@ -543,6 +561,10 @@ printage(const git_time *t) long d, n; size_t i, level; + if (!t || !validtime(t)) {+ fputs("<td class=\"r age a4\">—</td>", stdout);+ return;+ } if ((d = (long)(time(NULL) - (time_t)t->time)) < 0) d = 0; for (i = 0; d >= upto[i]; i++)@@ -557,18 +579,17 @@ printage(const git_time *t) } void-printdiff(git_diff *diff)+printdiff(git_diff *diff, git_patch **patches) {- git_patch *patch; const git_diff_delta *delta; const git_diff_hunk *hunk; const git_diff_line *line;- size_t i, h, l, nhunks, nlines, len;+ git_patch *p;+ size_t i, h, l, nlines, len, total = 0; for (i = 0; i < git_diff_num_deltas(diff); i++) {- if (git_patch_from_diff(&patch, diff, i) < 0)- continue;- delta = git_patch_get_delta(patch);+ delta = git_diff_get_delta(diff, i);+ p = patches[i]; printf("<h3 id=\"f%zu\">", i); if (strcmp(delta->old_file.path, delta->new_file.path)) { xmlencode(delta->old_file.path);@@ -578,9 +599,8 @@ printdiff(git_diff *diff) fputs("</h3>\n<pre class=\"diff\">", stdout); if (delta->flags & GIT_DIFF_FLAG_BINARY) fputs("<span>Binary files differ.</span>", stdout);- nhunks = git_patch_num_hunks(patch);- for (h = 0; h < nhunks; h++) {- if (git_patch_get_hunk(&hunk, &nlines, patch, h) < 0)+ for (h = 0; p && h < git_patch_num_hunks(p); h++) {+ if (git_patch_get_hunk(&hunk, &nlines, p, h) < 0) break; len = hunk->header_len; while (len && hunk->header[len - 1] == '\n')@@ -589,8 +609,10 @@ printdiff(git_diff *diff) xmlencoden(hunk->header, len); fputs("</span>", stdout); for (l = 0; l < nlines; l++) {- if (git_patch_get_line_in_hunk(&line, patch,- h, l) < 0)+ if (total++ >= (size_t)maxdiff)+ goto cut;+ if (git_patch_get_line_in_hunk(&line, p, h,+ l) < 0) break; if (line->origin == GIT_DIFF_LINE_ADDITION) fputs("<span class=\"i\">+", stdout);@@ -609,8 +631,10 @@ printdiff(git_diff *diff) } } fputs("</pre>\n", stdout);- git_patch_free(patch); }+ return;+cut:+ fputs("</pre>\n<p>Diff truncated.</p>\n", stdout); } void@@ -662,36 +686,33 @@ printpath(const char *path) } void-printstat(git_diff *diff)+printstat(git_diff *diff, git_patch **patches) {- git_patch *patch;- size_t i, j, n, add, del, max = 0, tadd = 0, tdel = 0;- size_t *st;+ size_t i, j, n, add, del, w, max = 0, tadd = 0, tdel = 0; n = git_diff_num_deltas(diff);- if (!(st = calloc(n * 2 + 1, sizeof(*st))))- return; for (i = 0; i < n; i++) {- if (git_patch_from_diff(&patch, diff, i) < 0)- continue;- git_patch_line_stats(NULL, &st[i * 2], &st[i * 2 + 1], patch);- git_patch_free(patch);- if (st[i * 2] + st[i * 2 + 1] > max)- max = st[i * 2] + st[i * 2 + 1];+ add = del = 0;+ if (patches[i])+ git_patch_line_stats(NULL, &add, &del, patches[i]);+ if (add + del > max)+ max = add + del; } fputs("<table class=\"stat\">\n", stdout); for (i = 0; i < n; i++) {- add = st[i * 2];- del = st[i * 2 + 1];+ add = del = 0;+ if (patches[i])+ git_patch_line_stats(NULL, &add, &del, patches[i]); tadd += add; tdel += del; printf("<tr><td><a href=\"#f%zu\">", i); xmlencode(git_diff_get_delta(diff, i)->new_file.path); printf("</a></td><td class=\"r num\">%zu</td>" "<td class=\"bar\"><span class=\"ins\">", add + del);- if (max > (size_t)barwidth) {- add = (add * barwidth + max - 1) / max;- del = (del * barwidth + max - 1) / max;+ if (add + del && max > (size_t)barwidth) {+ w = ((add + del) * barwidth + max - 1) / max;+ add = (add * w + (add + del) / 2) / (add + del);+ del = w - add; } for (j = 0; j < add; j++) putchar('+');@@ -704,7 +725,6 @@ printstat(git_diff *diff) "<span class=\"del\">-%zu</span>/" "<span class=\"ins\">+%zu</span></p>\n", n, n == 1 ? "" : "s", tdel, tadd);- free(st); } void@@ -714,6 +734,8 @@ printtime(const git_time *t, const char *fmt) time_t secs; char buf[32]; + if (!validtime(t))+ return; secs = (time_t)t->time + (time_t)t->offset * 60; if (!gmtime_r(&secs, &tm)) return;@@ -766,14 +788,6 @@ end: } void-readdesc(const char *name, char *buf, size_t len)-{- readfile(name, "description", buf, len);- if (!strncmp(buf, "Unnamed repository", 18))- buf[0] = '\0';-}--void readfile(const char *name, const char *file, char *buf, size_t len) { char path[PATH_MAX];@@ -787,6 +801,8 @@ readfile(const char *name, const char *file, char *buf, size_t len) if (fgets(buf, (int)len, fp)) buf[strcspn(buf, "\r\n")] = '\0'; fclose(fp);+ if (!strncmp(buf, "Unnamed repository", 18))+ buf[0] = '\0'; } void@@ -841,7 +857,7 @@ repoheader(const char *tab) char desc[256]; size_t i; - readdesc(reponame, desc, sizeof(desc));+ readfile(reponame, "description", desc, sizeof(desc)); fputs("<header>\n<h1>", stdout); printname(reponame); fputs("</h1>\n", stdout);@@ -894,9 +910,9 @@ route(char *path) *cut = '\0'; rest = cut + 1; }- if (openrepo(&repo, path) < 0) {+ if (openrepo(&repo, path) < 0 || git_repository_odb(&odb, repo) < 0) { notfound();- return;+ goto end; } reponame = path; if (git_repository_head(&r, repo) >= 0)@@ -929,6 +945,8 @@ route(char *path) } else { notfound(); }+end:+ git_odb_free(odb); git_repository_free(repo); } @@ -977,8 +995,8 @@ treerow(const git_tree_entry *e, const char *path) { char child[PATH_MAX]; const char *name;- git_object_t type;- git_blob *blob;+ git_object_t type, otype;+ size_t len; name = git_tree_entry_name(e); type = git_tree_entry_type(e);@@ -998,44 +1016,29 @@ treerow(const git_tree_entry *e, const char *path) fputs(type == GIT_OBJECT_TREE ? "/</a>" : "</a>", stdout); } fputs("</td><td class=\"r num\">", stdout);- if (type == GIT_OBJECT_BLOB- && git_blob_lookup(&blob, repo, git_tree_entry_id(e)) >= 0) {- printf("%lluB", (unsigned long long)git_blob_rawsize(blob));- git_blob_free(blob);- }+ if (type == GIT_OBJECT_BLOB && git_odb_read_header(&len, &otype, odb,+ git_tree_entry_id(e)) >= 0)+ printf("%zuB", len); fputs("</td></tr>\n", stdout); } void treetable(const git_tree *tree, const char *path) {- char parent[PATH_MAX]; const git_tree_entry *e;- const char *p; size_t i, n;- int pass, isdir;+ int pass; fputs("<table class=\"tree\">\n<thead><tr><th class=\"sm\">Mode</th>" "<th>Name</th><th class=\"r\">Size</th></tr></thead>\n" "<tbody>\n", stdout);- if (*path) {- p = strrchr(path, '/');- if (bprintf(parent, sizeof(parent), "%.*s",- p ? (int)(p - path) : 0, path) < 0)- return;- fputs("<tr><td class=\"mode sm\">d---------</td>"- "<td class=\"dirent\"><a href=\"", stdout);- printurl("tree", ref, parent);- fputs("\">../</a></td><td></td></tr>\n", stdout);- } n = git_tree_entrycount(tree); for (pass = 0; pass < 2; pass++) { for (i = 0; i < n; i++) { e = git_tree_entry_byindex(tree, i);- isdir = git_tree_entry_type(e) == GIT_OBJECT_TREE;- if (isdir == pass)- continue;- treerow(e, path);+ if ((git_tree_entry_type(e) == GIT_OBJECT_TREE)+ != pass)+ treerow(e, path); } } fputs("</tbody>\n</table>\n", stdout);@@ -1077,49 +1080,46 @@ validname(const char *s) return !start; } -void-walk(const char *rel, int depth)+int+validtime(const git_time *t) {- char dir[PATH_MAX];- struct dirent **ents;- int i, n;-- if (depth > maxdepth)- return;- if (bprintf(dir, sizeof(dir), "%s/%s", root, rel) < 0)- return;- if ((n = scandir(dir, &ents, NULL, alphasort)) < 0)- return;- for (i = 0; i < n; i++) {- walkent(dir, rel, ents[i]->d_name, depth);- free(ents[i]);- }- free(ents);+ return t->time >= 0 && t->time <= MAXTIME+ && t->offset >= -1440 && t->offset <= 1440; } void-walkent(const char *dir, const char *rel, const char *e, int depth)+walk(const char *rel, int depth) { char path[PATH_MAX], name[PATH_MAX];+ struct dirent **ents; struct stat st;+ const char *e; size_t len;+ int i, n; - if (e[0] == '.')- return;- if (bprintf(path, sizeof(path), "%s/%s", dir, e) < 0)- return;- if (lstat(path, &st) < 0 || !S_ISDIR(st.st_mode))- return;- if (bprintf(name, sizeof(name), "%s%s%s", rel, *rel ? "/" : "", e) < 0)- return;- len = strlen(e);- if (len <= 4 || strcmp(e + len - 4, ".git")) {- walk(name, depth + 1);+ if (depth > maxdepth+ || bprintf(path, sizeof(path), "%s/%s", root, rel) < 0+ || (n = scandir(path, &ents, NULL, alphasort)) < 0) return;+ for (i = 0; i < n; i++) {+ e = ents[i]->d_name;+ len = strlen(e);+ if (e[0] != '.'+ && bprintf(name, sizeof(name), "%s%s%s", rel, *rel ? "/" : "",+ e) >= 0+ && bprintf(path, sizeof(path), "%s/%s", root, name) >= 0+ && lstat(path, &st) >= 0 && S_ISDIR(st.st_mode)) {+ if (len > 4 && !strcmp(e + len - 4, ".git")) {+ name[strlen(name) - 4] = '\0';+ if (isrepo(name))+ indexrow(name);+ } else {+ walk(name, depth + 1);+ }+ }+ free(ents[i]); }- name[strlen(name) - 4] = '\0';- if (validname(name))- indexrow(name);+ free(ents); } void@@ -1148,9 +1148,12 @@ xmlencoden(const char *s, size_t len) int main(void) {- char path[PATH_MAX];+ char path[1024]; const char *q;+ struct rlimit rl; + rl.rlim_cur = rl.rlim_max = maxcpu;+ setrlimit(RLIMIT_CPU, &rl); root = envor("PETAL_ROOT", root); css = envor("PETAL_CSS", css); icon = envor("PETAL_ICON", icon);