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

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

 



On Wed, 14 Jun 2023 at 23:44, Jeff King <peff@xxxxxxxx> wrote:
>
> 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

Thanks Jeff for spotting and for the fix. I'll apply and add a test in patch v3.

> 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).

Good idea. Will do in patch v3.

>
> > @@ -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