Re: [PATCH v2 1/2] credential: avoid erasing distinct password

[Date Prev][Date Next][Thread Prev][Thread Next][Date Index][Thread Index]

 



On Wed, Jun 14, 2023 at 09:40:37PM +0000, M Hickford via GitGitGadget wrote:

> From: M Hickford <mirth.hickford@xxxxxxxxx>
> 
> Test that credential helpers do not erase a password distinct from the
> input. Such calls can happen when multiple credential helpers are
> configured.
> 
> Fixes for credential-cache and credential-store.

I think this goal makes sense. I missed the "multiple helpers" part of
your message at first, and wondered how you would even get such
duplicate entries stored in a helper. But the point is that you might
have two helpers with two separate passwords.

Which is...weird, I'd think, because we will only ever use one of them.
But from past discussions, I guess you're in a situation where you could
use some kind of renewable token _or_ possibly a more stable password,
via two different helpers. And when the token expires, you want to be
able to remove it from a caching helper without asking for the stable
password to be dropped.

I don't think it's strictly necessary to spell out the intended use in
the commit message. The behavior you're proposing seems like the
least-surprising thing to me in general (and it is only because the
situation is rare that nobody complained about the existing behavior in
the last decade). But it might not hurt to outline the case that we'd
expect this to help.

The other thing I'd watch out for is how this behavior would affect
non-erase modes. Let's see...

> diff --git a/builtin/credential-cache--daemon.c b/builtin/credential-cache--daemon.c
> index 756c5f02aef..82f376d3351 100644
> --- a/builtin/credential-cache--daemon.c
> +++ b/builtin/credential-cache--daemon.c
> @@ -33,12 +33,12 @@ static void cache_credential(struct credential *c, int timeout)
>  	e->expiration = time(NULL) + timeout;
>  }
>  
> -static struct credential_cache_entry *lookup_credential(const struct credential *c)
> +static struct credential_cache_entry *lookup_credential(const struct credential *c, int match_password)
>  {
>  	int i;
>  	for (i = 0; i < entries_nr; i++) {
>  		struct credential *e = &entries[i].item;
> -		if (credential_match(c, e))
> +		if (credential_match(c, e, match_password))
>  			return &entries[i];
>  	}
>  	return NULL;
> [...]
> @@ -127,7 +127,7 @@ static void serve_one_client(FILE *in, FILE *out)
>  	if (read_request(in, &c, &action, &timeout) < 0)
>  		/* ignore error */ ;
>  	else if (!strcmp(action.buf, "get")) {
> -		struct credential_cache_entry *e = lookup_credential(&c);
> +		struct credential_cache_entry *e = lookup_credential(&c, 0);
>  		if (e) {
>  			fprintf(out, "username=%s\n", e->item.username);
>  			fprintf(out, "password=%s\n", e->item.password);

OK, for a cache "get" we'll continue not to match the password. This
shouldn't trigger in practice, as Git will stop asking helpers once it
has a username and password, but it is good to retain the existing
behavior here.

> @@ -48,7 +48,7 @@ static void remove_credential(const struct credential *c)
>  {
>  	struct credential_cache_entry *e;
>  
> -	e = lookup_credential(c);
> +	e = lookup_credential(c, c->password != NULL);
>  	if (e)
>  		e->expiration = 0;
>  }

And this is erase, where we do want to respect it. I actually think
passing an unconditional "1" here instead of the NULL check would be OK,
as the CHECK() macro in credential_match() treats NULL as "always
match". But I am also OK with making that logic explicit here.

This made me wonder how "store" works, since it's not touched here. It's
implemented as remove_credential(), plus an unconditional allocation via
cache_credential().

It seems like overwriting would be broken, then, wouldn't it? If I
store:

   https://user:pass1@xxxxxxxxxxx

and then try to store:

   https://user:pass2@xxxxxxxxxxx

then the call to remove_credential() will see a "struct credential" with
the password set to "pass2". And because it always passes
match_password, it will fail to remove it, and we'll end up with two.

We should be able to test that pretty easily, like this:

  echo url=https://user:pass1@xxxxxxxxxxx | git credential-cache store
  echo url=https://user:pass2@xxxxxxxxxxx | git credential-cache store
  echo url=https://example.com | git credential-cache get

Before your patch that will return "pass1". After, it returns "pass2",
because the old credential is left in place. I think you'd want
something like:

diff --git a/builtin/credential-cache--daemon.c b/builtin/credential-cache--daemon.c
index 82f376d335..f64dd21d33 100644
--- a/builtin/credential-cache--daemon.c
+++ b/builtin/credential-cache--daemon.c
@@ -44,11 +44,11 @@ static struct credential_cache_entry *lookup_credential(const struct credential
 	return NULL;
 }
 
-static void remove_credential(const struct credential *c)
+static void remove_credential(const struct credential *c, int match_password)
 {
 	struct credential_cache_entry *e;
 
-	e = lookup_credential(c, c->password != NULL);
+	e = lookup_credential(c, match_password);
 	if (e)
 		e->expiration = 0;
 }
@@ -151,14 +151,14 @@ static void serve_one_client(FILE *in, FILE *out)
 		exit(0);
 	}
 	else if (!strcmp(action.buf, "erase"))
-		remove_credential(&c);
+		remove_credential(&c, 1);
 	else if (!strcmp(action.buf, "store")) {
 		if (timeout < 0)
 			warning("cache client didn't specify a timeout");
 		else if (!c.username || !c.password)
 			warning("cache client gave us a partial credential");
 		else {
-			remove_credential(&c);
+			remove_credential(&c, 0);
 			cache_credential(&c, timeout);
 		}
 	}

Unless your goal really is to store two entries that differ only in
their passwords within a single cache. That seems weird, though. Again,
it might help if your use case was fleshed out a bit more.

> diff --git a/builtin/credential-store.c b/builtin/credential-store.c
> index 30c6ccf56c0..e0ae028b1c3 100644
> --- a/builtin/credential-store.c
> +++ b/builtin/credential-store.c
> @@ -13,12 +13,14 @@ static struct lock_file credential_lock;
>  static int parse_credential_file(const char *fn,
>  				  struct credential *c,
>  				  void (*match_cb)(struct credential *),
> -				  void (*other_cb)(struct strbuf *))
> +				  void (*other_cb)(struct strbuf *),
> +				  const char* op)
>  {
>  	FILE *fh;
>  	struct strbuf line = STRBUF_INIT;
>  	struct credential entry = CREDENTIAL_INIT;
>  	int found_credential = 0;
> +	const int match_password = !strcmp(op, "erase") && c->password != NULL;
>  
>  	fh = fopen(fn, "r");
>  	if (!fh) {
> @@ -29,8 +31,8 @@ static int parse_credential_file(const char *fn,
>  
>  	while (strbuf_getline_lf(&line, fh) != EOF) {
>  		if (!credential_from_url_gently(&entry, line.buf, 1) &&
> -		    entry.username && entry.password &&
> -		    credential_match(c, &entry)) {
> +			entry.username && entry.password &&
> +			credential_match(c, &entry, match_password)) {
>  			found_credential = 1;
>  			if (match_cb) {
>  				match_cb(&entry);

OK, so for credential-store we only kick in the new behavior for
"erase". So it would not have the same quirk that I mentioned above.

I do think your logic could likewise here be:

  const int match_password = !strcmp(op, "erase");

because credential_match() handles the NULL case already.

(It's also unusual for us to declare non-parameter variables as "const";
it's not wrong to do so, so it's really just a style thing, but there's
not much benefit).

> @@ -60,7 +62,7 @@ static void print_line(struct strbuf *buf)
>  }
>  
>  static void rewrite_credential_file(const char *fn, struct credential *c,
> -				    struct strbuf *extra)
> +				    struct strbuf *extra, const char *op)
>  {
>  	int timeout_ms = 1000;
>  
> @@ -69,7 +71,7 @@ static void rewrite_credential_file(const char *fn, struct credential *c,
>  		die_errno(_("unable to get credential storage lock in %d ms"), timeout_ms);
>  	if (extra)
>  		print_line(extra);
> -	parse_credential_file(fn, c, NULL, print_line);
> +	parse_credential_file(fn, c, NULL, print_line, op);
>  	if (commit_lock_file(&credential_lock) < 0)
>  		die_errno("unable to write credential store");
>  }

If we have to plumb through this "op" string, I wonder if it would be
more flexible and robust to just pass along a match_password flag.
That's generally a bit more robust than string matching (where a
misspelling in one string would not be caught by the compiler, unlike a
boolean variable).

> @@ -221,6 +222,31 @@ helper_test() {
>  		EOF
>  	'
>  
> +	test_expect_success "helper ($HELPER) does not erase a password distinct from input" '
> +		check approve $HELPER <<-\EOF &&
> +		protocol=https
> +		host=example.com
> +		username=user5
> +		password=pass1
> +		EOF
> +		check reject $HELPER <<-\EOF &&
> +		protocol=https
> +		host=example.com
> +		username=user5
> +		password=pass2
> +		EOF
> +		check fill $HELPER <<-\EOF
> +		protocol=https
> +		host=example.com
> +		username=user5
> +		--
> +		protocol=https
> +		host=example.com
> +		username=user5
> +		password=pass1
> +		EOF
> +	'

The test makes sense. We are not using multiple helpers, but the
behavior can be checked using a single helper, since the point is that
it should be independent of what any other helper is doing.

-Peff



[Index of Archives]     [Linux Kernel Development]     [Gcc Help]     [IETF Annouce]     [DCCP]     [Netdev]     [Networking]     [Security]     [V4L]     [Bugtraq]     [Yosemite]     [MIPS Linux]     [ARM Linux]     [Linux Security]     [Linux RAID]     [Linux SCSI]     [Fedora Users]

  Powered by Linux