tl;dr Moderately interesting and high-impact directory traversal bug made worse because of snprintf, awesome bug bounty response. CVE-2017-12843

As I began to cope with the impending loss of my university email address, I decided to use the opportunity to migrate my mail from Gmail to FastMail. I made this change for a number of reasons, but their privacy policy was a big one. Their web interface and mobile app are also really good.

Another plus to FastMail is their awesome bug bounty program.

FastMail is mostly public about their software stack. Their IMAP and POP3 servers are run on Cyrus IMAP server. I cloned the repo and looked for the main IMAP REPL.

In imap/imapd.c, I started going over each of the commands accessible by regular users. I came across the following:

else if (!strcmp(cmd.s, "Syncapply")) {
	struct dlist *kl = sync_parseline(imapd_in);

	if (kl) {
		cmd_syncapply(tag.s, kl, reserve_list);
	else goto extraargs;

I didn’t think sync_parseline looked particularly interesting, but this was already my second pass over this code so I decided to be a bit more thorough. I followed sync_parseline over to dlist_parse in dlist.c:

c = getastring(in, NULL, &pbuf);
if (c != ' ') goto fail;
c = getastring(in, NULL, &gbuf);
if (c != ' ') goto fail;
c = getuint32(in, &size);
if (c != '}') goto fail;
c = prot_getc(in);
if (c == '\r') c = prot_getc(in);
if (c != '\n') goto fail;
if (!message_guid_decode(&tmp_guid, gbuf.s)) goto fail;
part = alt_reserve_base ? alt_reserve_base : pbuf.s;
if (reservefile(in, part, &tmp_guid, size, &fname)) goto fail;
dl = dlist_setfile(NULL, kbuf.s, pbuf.s, &tmp_guid, size, fname);

Hmm… reservefile sounds interesting. Let’s check that out…

FILE *file;
char buf[8192+1];
int r = 0;

/* XXX - write to a temporary file then move in to place! */
*fname = dlist_reserve_path(part, /*isarchive*/0, guid);

/* remove any duplicates if they're still here */

file = fopen(*fname, "w+");

HMM… Hopefully dlist_reserve_path is sane?

static char buf[MAX_MAILBOX_PATH];
const char *base;

/* part can be either a configured partition name, or a path */
if (strchr(part, '/')) {
	base = part;
else {
	base = isarchive ? config_archivepartitiondir(part)
					 : config_partitiondir(part);

/* we expect to have a base at this point, so let's assert that */
assert(base != NULL);

snprintf(buf, MAX_MAILBOX_PATH, "%s/sync./%lu/%s",
			  base, (unsigned long)getpid(),

/* gotta make sure we can create files */
if (cyrus_mkdir(buf, 0755)) {
	/* it's going to fail later, but at least this will help */
	syslog(LOG_ERR, "IOERROR: failed to create %s/sync./%lu/ for reserve: %m",
					base, (unsigned long)getpid());
return buf;

Oops. We have complete control over base (read directly from user input), and it gets inserted as the path prefix in snprintf. This maybe wouldn’t be so bad if the snprintf didn’t mean that the maximum path length was capped, since we would have the /sync./ garbage always appended afterwards. But since we can make base more or less arbitrarily long, we can run that suffix off of the end and specify the entire file path. Later, code in reservefile allows the user to write arbitrary contents to the created file. Here’s a PoC that writes some stuff to tmp.

This is a pretty bad vulnerability and depending on your configuration could very easily lead to remote code execution. I emailed FastMail as per their bug bounty program, and in two hours (?!) they had a patch rolled out to their production servers and pushed upstream.