mirror of
https://git.zx2c4.com/cgit
synced 2024-11-23 17:08:41 +00:00
61d4147ea2
* parsing.c (substr): Handle tail < head. This started when I noticed some cgit segfaults on savannah.gnu.org. Finding the offending URL/commit and then constructing a stand-alone reproducer were far more time-consuming than writing the actual patch. The problem arises with a commit like this, in which the user name part of the "Author" field is empty: $ git log -1 commit 6f3f41d73393278f3ede68a2cb1e7a2a23fa3421 Author: <T at h.or> Date: Mon Apr 23 22:29:16 2012 +0200 Here's what happens: (this is due to buf=malloc(0); strncpy (buf, head, -1); where "head" may point to plenty of attacker-specified non-NUL bytes, so we can overwrite a zero-length heap buffer with arbitrary data) Invalid write of size 1 at 0x4A09361: strncpy (mc_replace_strmem.c:463) by 0x408977: substr (parsing.c:61) by 0x4089EF: parse_user (parsing.c:73) by 0x408D10: cgit_parse_commit (parsing.c:153) by 0x40A540: cgit_mk_refinfo (shared.c:171) by 0x40A581: cgit_refs_cb (shared.c:181) by 0x43DEB3: do_for_each_ref (refs.c:690) by 0x41075E: cgit_print_branches (ui-refs.c:191) by 0x416EF2: cgit_print_summary (ui-summary.c:56) by 0x40780A: summary_fn (cmd.c:120) by 0x40667A: process_request (cgit.c:544) by 0x404078: cache_process (cache.c:322) Address 0x4c718d0 is 0 bytes after a block of size 0 alloc'd at 0x4A0884D: malloc (vg_replace_malloc.c:263) by 0x455C85: xmalloc (wrapper.c:35) by 0x40894C: substr (parsing.c:60) by 0x4089EF: parse_user (parsing.c:73) by 0x408D10: cgit_parse_commit (parsing.c:153) by 0x40A540: cgit_mk_refinfo (shared.c:171) by 0x40A581: cgit_refs_cb (shared.c:181) by 0x43DEB3: do_for_each_ref (refs.c:690) by 0x41075E: cgit_print_branches (ui-refs.c:191) by 0x416EF2: cgit_print_summary (ui-summary.c:56) by 0x40780A: summary_fn (cmd.c:120) by 0x40667A: process_request (cgit.c:544) Invalid write of size 1 at 0x4A09400: strncpy (mc_replace_strmem.c:463) by 0x408977: substr (parsing.c:61) by 0x4089EF: parse_user (parsing.c:73) by 0x408D10: cgit_parse_commit (parsing.c:153) by 0x40A540: cgit_mk_refinfo (shared.c:171) by 0x40A581: cgit_refs_cb (shared.c:181) by 0x43DEB3: do_for_each_ref (refs.c:690) by 0x41075E: cgit_print_branches (ui-refs.c:191) by 0x416EF2: cgit_print_summary (ui-summary.c:56) by 0x40780A: summary_fn (cmd.c:120) by 0x40667A: process_request (cgit.c:544) by 0x404078: cache_process (cache.c:322) Address 0x4c7192b is not stack'd, malloc'd or (recently) free'd Invalid write of size 1 at 0x4A0940E: strncpy (mc_replace_strmem.c:463) by 0x408977: substr (parsing.c:61) by 0x4089EF: parse_user (parsing.c:73) by 0x408D10: cgit_parse_commit (parsing.c:153) by 0x40A540: cgit_mk_refinfo (shared.c:171) by 0x40A581: cgit_refs_cb (shared.c:181) by 0x43DEB3: do_for_each_ref (refs.c:690) by 0x41075E: cgit_print_branches (ui-refs.c:191) by 0x416EF2: cgit_print_summary (ui-summary.c:56) by 0x40780A: summary_fn (cmd.c:120) by 0x40667A: process_request (cgit.c:544) by 0x404078: cache_process (cache.c:322) Address 0x4c7192d is not stack'd, malloc'd or (recently) free'd Process terminating with default action of signal 11 (SIGSEGV) Access not within mapped region at address 0x502F000 at 0x4A09400: strncpy (mc_replace_strmem.c:463) by 0x408977: substr (parsing.c:61) by 0x4089EF: parse_user (parsing.c:73) by 0x408D10: cgit_parse_commit (parsing.c:153) by 0x40A540: cgit_mk_refinfo (shared.c:171) by 0x40A581: cgit_refs_cb (shared.c:181) by 0x43DEB3: do_for_each_ref (refs.c:690) by 0x41075E: cgit_print_branches (ui-refs.c:191) by 0x416EF2: cgit_print_summary (ui-summary.c:56) by 0x40780A: summary_fn (cmd.c:120) by 0x40667A: process_request (cgit.c:544) by 0x404078: cache_process (cache.c:322) This happens when tail - head == -1 here: (parsing.c) char *substr(const char *head, const char *tail) { char *buf; buf = xmalloc(tail - head + 1); strncpy(buf, head, tail - head); buf[tail - head] = '\0'; return buf; } char *parse_user(char *t, char **name, char **email, unsigned long *date) { char *p = t; int mode = 1; while (p && *p) { if (mode == 1 && *p == '<') { *name = substr(t, p - 1); t = p; mode++; } else if (mode == 1 && *p == '\n') { The fix is to handle the case of (tail < head) before calling xmalloc, thus avoiding passing an invalid value to xmalloc. And here's the reproducer: It was tricky to reproduce, because git prohibits use of an empty "name" in a commit ID. To construct the offending commit, I had to resort to using "git hash-object". git init -q foo && ( cd foo && echo a > j && git add . && git ci -q --author='au <T at h.or>' -m. . && h=$(git cat-file commit HEAD|sed 's/au //' \ |git hash-object -t commit -w --stdin) && git co -q -b test $h && git br -q -D master && git br -q -m test master) git clone -q --bare foo foo.git cat <<EOF > in repo.url=foo.git repo.path=foo.git EOF CGIT_CONFIG=in QUERY_STRING=url=foo.git valgrind ./cgit The valgrind output is what you see above. AFAICS, this is not exploitable thanks (ironically) to the use of strncpy. Since that -1 translates to SIZE_MAX and this is strncpy, not only does it copy whatever is in "head" (up to first NUL), but it also writes SIZE_MAX - strlen(head) NUL bytes into the destination buffer, and that latter is guaranteed to evoke a segfault. Since cgit is single-threaded, AFAICS, there is no way that the buffer clobbering can be turned into an exploit.
261 lines
4.9 KiB
C
261 lines
4.9 KiB
C
/* config.c: parsing of config files
|
|
*
|
|
* Copyright (C) 2006 Lars Hjemli
|
|
*
|
|
* Licensed under GNU General Public License v2
|
|
* (see COPYING for full license text)
|
|
*/
|
|
|
|
#include "cgit.h"
|
|
|
|
/*
|
|
* url syntax: [repo ['/' cmd [ '/' path]]]
|
|
* repo: any valid repo url, may contain '/'
|
|
* cmd: log | commit | diff | tree | view | blob | snapshot
|
|
* path: any valid path, may contain '/'
|
|
*
|
|
*/
|
|
void cgit_parse_url(const char *url)
|
|
{
|
|
char *cmd, *p;
|
|
|
|
ctx.repo = NULL;
|
|
if (!url || url[0] == '\0')
|
|
return;
|
|
|
|
ctx.repo = cgit_get_repoinfo(url);
|
|
if (ctx.repo) {
|
|
ctx.qry.repo = ctx.repo->url;
|
|
return;
|
|
}
|
|
|
|
cmd = strchr(url, '/');
|
|
while (!ctx.repo && cmd) {
|
|
cmd[0] = '\0';
|
|
ctx.repo = cgit_get_repoinfo(url);
|
|
if (ctx.repo == NULL) {
|
|
cmd[0] = '/';
|
|
cmd = strchr(cmd + 1, '/');
|
|
continue;
|
|
}
|
|
|
|
ctx.qry.repo = ctx.repo->url;
|
|
p = strchr(cmd + 1, '/');
|
|
if (p) {
|
|
p[0] = '\0';
|
|
if (p[1])
|
|
ctx.qry.path = trim_end(p + 1, '/');
|
|
}
|
|
if (cmd[1])
|
|
ctx.qry.page = xstrdup(cmd + 1);
|
|
return;
|
|
}
|
|
}
|
|
|
|
char *substr(const char *head, const char *tail)
|
|
{
|
|
char *buf;
|
|
|
|
if (tail < head)
|
|
return xstrdup("");
|
|
buf = xmalloc(tail - head + 1);
|
|
strncpy(buf, head, tail - head);
|
|
buf[tail - head] = '\0';
|
|
return buf;
|
|
}
|
|
|
|
char *parse_user(char *t, char **name, char **email, unsigned long *date)
|
|
{
|
|
char *p = t;
|
|
int mode = 1;
|
|
|
|
while (p && *p) {
|
|
if (mode == 1 && *p == '<') {
|
|
*name = substr(t, p - 1);
|
|
t = p;
|
|
mode++;
|
|
} else if (mode == 1 && *p == '\n') {
|
|
*name = substr(t, p);
|
|
p++;
|
|
break;
|
|
} else if (mode == 2 && *p == '>') {
|
|
*email = substr(t, p + 1);
|
|
t = p;
|
|
mode++;
|
|
} else if (mode == 2 && *p == '\n') {
|
|
*email = substr(t, p);
|
|
p++;
|
|
break;
|
|
} else if (mode == 3 && isdigit(*p)) {
|
|
*date = atol(p);
|
|
mode++;
|
|
} else if (*p == '\n') {
|
|
p++;
|
|
break;
|
|
}
|
|
p++;
|
|
}
|
|
return p;
|
|
}
|
|
|
|
#ifdef NO_ICONV
|
|
#define reencode(a, b, c)
|
|
#else
|
|
const char *reencode(char **txt, const char *src_enc, const char *dst_enc)
|
|
{
|
|
char *tmp;
|
|
|
|
if (!txt)
|
|
return NULL;
|
|
|
|
if (!*txt || !src_enc || !dst_enc)
|
|
return *txt;
|
|
|
|
/* no encoding needed if src_enc equals dst_enc */
|
|
if(!strcasecmp(src_enc, dst_enc))
|
|
return *txt;
|
|
|
|
tmp = reencode_string(*txt, dst_enc, src_enc);
|
|
if (tmp) {
|
|
free(*txt);
|
|
*txt = tmp;
|
|
}
|
|
return *txt;
|
|
}
|
|
#endif
|
|
|
|
struct commitinfo *cgit_parse_commit(struct commit *commit)
|
|
{
|
|
struct commitinfo *ret;
|
|
char *p = commit->buffer, *t;
|
|
|
|
ret = xmalloc(sizeof(*ret));
|
|
ret->commit = commit;
|
|
ret->author = NULL;
|
|
ret->author_email = NULL;
|
|
ret->committer = NULL;
|
|
ret->committer_email = NULL;
|
|
ret->subject = NULL;
|
|
ret->msg = NULL;
|
|
ret->msg_encoding = NULL;
|
|
|
|
if (p == NULL)
|
|
return ret;
|
|
|
|
if (strncmp(p, "tree ", 5))
|
|
die("Bad commit: %s", sha1_to_hex(commit->object.sha1));
|
|
else
|
|
p += 46; // "tree " + hex[40] + "\n"
|
|
|
|
while (!strncmp(p, "parent ", 7))
|
|
p += 48; // "parent " + hex[40] + "\n"
|
|
|
|
if (p && !strncmp(p, "author ", 7)) {
|
|
p = parse_user(p + 7, &ret->author, &ret->author_email,
|
|
&ret->author_date);
|
|
}
|
|
|
|
if (p && !strncmp(p, "committer ", 9)) {
|
|
p = parse_user(p + 9, &ret->committer, &ret->committer_email,
|
|
&ret->committer_date);
|
|
}
|
|
|
|
if (p && !strncmp(p, "encoding ", 9)) {
|
|
p += 9;
|
|
t = strchr(p, '\n');
|
|
if (t) {
|
|
ret->msg_encoding = substr(p, t + 1);
|
|
p = t + 1;
|
|
}
|
|
}
|
|
|
|
/* if no special encoding is found, assume UTF-8 */
|
|
if(!ret->msg_encoding)
|
|
ret->msg_encoding = xstrdup("UTF-8");
|
|
|
|
// skip unknown header fields
|
|
while (p && *p && (*p != '\n')) {
|
|
p = strchr(p, '\n');
|
|
if (p)
|
|
p++;
|
|
}
|
|
|
|
// skip empty lines between headers and message
|
|
while (p && *p == '\n')
|
|
p++;
|
|
|
|
if (!p)
|
|
return ret;
|
|
|
|
t = strchr(p, '\n');
|
|
if (t) {
|
|
ret->subject = substr(p, t);
|
|
p = t + 1;
|
|
|
|
while (p && *p == '\n') {
|
|
p = strchr(p, '\n');
|
|
if (p)
|
|
p++;
|
|
}
|
|
if (p)
|
|
ret->msg = xstrdup(p);
|
|
} else
|
|
ret->subject = xstrdup(p);
|
|
|
|
reencode(&ret->author, ret->msg_encoding, PAGE_ENCODING);
|
|
reencode(&ret->author_email, ret->msg_encoding, PAGE_ENCODING);
|
|
reencode(&ret->committer, ret->msg_encoding, PAGE_ENCODING);
|
|
reencode(&ret->committer_email, ret->msg_encoding, PAGE_ENCODING);
|
|
reencode(&ret->subject, ret->msg_encoding, PAGE_ENCODING);
|
|
reencode(&ret->msg, ret->msg_encoding, PAGE_ENCODING);
|
|
|
|
return ret;
|
|
}
|
|
|
|
|
|
struct taginfo *cgit_parse_tag(struct tag *tag)
|
|
{
|
|
void *data;
|
|
enum object_type type;
|
|
unsigned long size;
|
|
char *p;
|
|
struct taginfo *ret;
|
|
|
|
data = read_sha1_file(tag->object.sha1, &type, &size);
|
|
if (!data || type != OBJ_TAG) {
|
|
free(data);
|
|
return 0;
|
|
}
|
|
|
|
ret = xmalloc(sizeof(*ret));
|
|
ret->tagger = NULL;
|
|
ret->tagger_email = NULL;
|
|
ret->tagger_date = 0;
|
|
ret->msg = NULL;
|
|
|
|
p = data;
|
|
|
|
while (p && *p) {
|
|
if (*p == '\n')
|
|
break;
|
|
|
|
if (!strncmp(p, "tagger ", 7)) {
|
|
p = parse_user(p + 7, &ret->tagger, &ret->tagger_email,
|
|
&ret->tagger_date);
|
|
} else {
|
|
p = strchr(p, '\n');
|
|
if (p)
|
|
p++;
|
|
}
|
|
}
|
|
|
|
// skip empty lines between headers and message
|
|
while (p && *p == '\n')
|
|
p++;
|
|
|
|
if (p && *p)
|
|
ret->msg = xstrdup(p);
|
|
free(data);
|
|
return ret;
|
|
}
|