git/contrib/credential/osxkeychain/git-credential-osxkeychain.c
Taylor Blau 5747c8072b contrib/credential: avoid fixed-size buffer in osxkeychain
The macOS Keychain-based credential helper reads the newline-delimited
protocol stream one line at a time by repeatedly calling fgets() into a
fixed-size buffer, and is thus affected by the vulnerability described
in the previous commit.

To mitigate this attack, avoid using a fixed-size buffer, and instead
rely on getline() to allocate a buffer as large as necessary to fit the
entire content of the line, preventing any protocol injection.

We solved a similar problem in a5bb10fd5e (config: avoid fixed-sized
buffer when renaming/deleting a section, 2023-04-06) by switching to
strbuf_getline(). We can't do that here because the contrib helpers do
not link with the rest of Git, and so can't use a strbuf. But we can use
the system getline() directly, which works similarly.

In most parts of Git we don't assume that every platform has getline().
But this helper is run only on OS X, and that platform added support in
10.7 ("Lion") which was released in 2011.

Tested-by: Taylor Blau <me@ttaylorr.com>
Co-authored-by: Jeff King <peff@peff.net>
Signed-off-by: Jeff King <peff@peff.net>
Signed-off-by: Taylor Blau <me@ttaylorr.com>
Signed-off-by: Junio C Hamano <gitster@pobox.com>
2023-05-01 09:27:01 -07:00

194 lines
4.1 KiB
C

#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <Security/Security.h>
static SecProtocolType protocol;
static char *host;
static char *path;
static char *username;
static char *password;
static UInt16 port;
__attribute__((format (printf, 1, 2)))
static void die(const char *err, ...)
{
char msg[4096];
va_list params;
va_start(params, err);
vsnprintf(msg, sizeof(msg), err, params);
fprintf(stderr, "%s\n", msg);
va_end(params);
exit(1);
}
static void *xstrdup(const char *s1)
{
void *ret = strdup(s1);
if (!ret)
die("Out of memory");
return ret;
}
#define KEYCHAIN_ITEM(x) (x ? strlen(x) : 0), x
#define KEYCHAIN_ARGS \
NULL, /* default keychain */ \
KEYCHAIN_ITEM(host), \
0, NULL, /* account domain */ \
KEYCHAIN_ITEM(username), \
KEYCHAIN_ITEM(path), \
port, \
protocol, \
kSecAuthenticationTypeDefault
static void write_item(const char *what, const char *buf, int len)
{
printf("%s=", what);
fwrite(buf, 1, len, stdout);
putchar('\n');
}
static void find_username_in_item(SecKeychainItemRef item)
{
SecKeychainAttributeList list;
SecKeychainAttribute attr;
list.count = 1;
list.attr = &attr;
attr.tag = kSecAccountItemAttr;
if (SecKeychainItemCopyContent(item, NULL, &list, NULL, NULL))
return;
write_item("username", attr.data, attr.length);
SecKeychainItemFreeContent(&list, NULL);
}
static void find_internet_password(void)
{
void *buf;
UInt32 len;
SecKeychainItemRef item;
if (SecKeychainFindInternetPassword(KEYCHAIN_ARGS, &len, &buf, &item))
return;
write_item("password", buf, len);
if (!username)
find_username_in_item(item);
SecKeychainItemFreeContent(NULL, buf);
}
static void delete_internet_password(void)
{
SecKeychainItemRef item;
/*
* Require at least a protocol and host for removal, which is what git
* will give us; if you want to do something more fancy, use the
* Keychain manager.
*/
if (!protocol || !host)
return;
if (SecKeychainFindInternetPassword(KEYCHAIN_ARGS, 0, NULL, &item))
return;
SecKeychainItemDelete(item);
}
static void add_internet_password(void)
{
/* Only store complete credentials */
if (!protocol || !host || !username || !password)
return;
if (SecKeychainAddInternetPassword(
KEYCHAIN_ARGS,
KEYCHAIN_ITEM(password),
NULL))
return;
}
static void read_credential(void)
{
char *buf = NULL;
size_t alloc;
ssize_t line_len;
while ((line_len = getline(&buf, &alloc, stdin)) > 0) {
char *v;
if (!strcmp(buf, "\n"))
break;
buf[line_len-1] = '\0';
v = strchr(buf, '=');
if (!v)
die("bad input: %s", buf);
*v++ = '\0';
if (!strcmp(buf, "protocol")) {
if (!strcmp(v, "imap"))
protocol = kSecProtocolTypeIMAP;
else if (!strcmp(v, "imaps"))
protocol = kSecProtocolTypeIMAPS;
else if (!strcmp(v, "ftp"))
protocol = kSecProtocolTypeFTP;
else if (!strcmp(v, "ftps"))
protocol = kSecProtocolTypeFTPS;
else if (!strcmp(v, "https"))
protocol = kSecProtocolTypeHTTPS;
else if (!strcmp(v, "http"))
protocol = kSecProtocolTypeHTTP;
else if (!strcmp(v, "smtp"))
protocol = kSecProtocolTypeSMTP;
else /* we don't yet handle other protocols */
exit(0);
}
else if (!strcmp(buf, "host")) {
char *colon = strchr(v, ':');
if (colon) {
*colon++ = '\0';
port = atoi(colon);
}
host = xstrdup(v);
}
else if (!strcmp(buf, "path"))
path = xstrdup(v);
else if (!strcmp(buf, "username"))
username = xstrdup(v);
else if (!strcmp(buf, "password"))
password = xstrdup(v);
/*
* Ignore other lines; we don't know what they mean, but
* this future-proofs us when later versions of git do
* learn new lines, and the helpers are updated to match.
*/
}
free(buf);
}
int main(int argc, const char **argv)
{
const char *usage =
"usage: git credential-osxkeychain <get|store|erase>";
if (!argv[1])
die("%s", usage);
read_credential();
if (!strcmp(argv[1], "get"))
find_internet_password();
else if (!strcmp(argv[1], "store"))
add_internet_password();
else if (!strcmp(argv[1], "erase"))
delete_internet_password();
/* otherwise, ignore unknown action */
return 0;
}