Github App Installation Callback Verification

I’m playing with Github Apps to learn how I can integrate with a multi-tenanted app. I need to associate Github Apps Installation’s (id) with the account id on my app. I found that state parameter can be used to achieve it, however I did not understand how it is secure.

Because the redirection URL can be only in the form of ?state=xyz&installation_id=1234 . How does one enusre the installation_id is not forged. A user can simply get the redirect URL and make whatever they want to access another installations. Only thing I can think of is keeping the installation_ids unique and prevent it to be used by other tenants. Hope it’s clear. Thanks for all assistance.

  • you will get an event about this installation
  • you can query github about this installation and see if it exists

The server for a GitHub App will receive an event, but it will also receive events from every other installation of apps that it hosts. From what I can tell, the only thing that ties that event to the user’s in-browser installation flow is the installation ID.

However, the installation ID appears to come from a sequence. It would be trivial to script those back-to-server redirects (with ?state=foo...&installation_id=123...) to rapidly and cheaply try 1000s of installation IDs. If any of those match, the attacker has now associated their account with another person’s account or organisation. At best this will leak information.

If the installation ID were a random UUID, say, then it would be effectively unguessable and there would be no problem.

You can also send along a state parameter that is passed back. I use it for sending an encrypted state (e.g. userId on my side of the requesting user). You can use this to guard against this attack vector and e.g. disable the user.

The user is already logged in to our site. The state parameter is useful for restoring the UI, but it doesn’t serve a security purpose. Even if state is signed or unguessable or something cryptographically secure, an attacker can obtain valid state values by following the same install-the-GitHub-app flow that other users follow, before forging installation_id on the return. We could detect if a user is doing this too frequently (albeit with more code) but it would only slow down a determined attacker, not prevent them.

Assuming you are storing the installationId with the user and send the userId with the state: when an attacker wants to steal an installationId, you can still check if you already know the installationId. If yes, you can check if the user and the installationId go together. So the attacker would only be able to guess the installation id of an user if he could also impersonate the user on your side. Otherwise the state would never include the userId. Or am I missing something?

That sounds prudent, i.e. an installation ID can only be associated with one user on my side.

There remains a small window of opportunity during the initial installation flow:

  • The user installs our GitHub App to to their account (or organisation).
  • <window of opportunity>
  • GitHub redirects the user back to our site with installation_id=... and we associate the user with the installation.

In that intervening moment, the app is installed but our application has not associated the user with that new installation. The window might be a few seconds, but an automated attack could:

  • Obtain a recent installation ID. Assuming installation IDs are sequential, a separate process could reinstall an app (any app) to get this.
  • Try our app’s Setup URL with a range of the next 10, 100, 1000 installation IDs.
  • Repeat.

and keep doing that indefinitely until it gets a hit. The window may be small, but it’s there, and an automated attack won’t get bored trying.

I think there is a way to prevent this window from being a threat:

  1. Authorise the user during the GitHub App installation flow, such that there is a secure OAuth exchange.
  2. Obtain an access token for the user, then check which installations the user has access to and thus validate the installation_id that we received.

I think this would strongly verify that the user and the installation are linked.

Can you see holes in that approach?

But the attacker needs the state variable, that you only passed out to the user, that you authenticated. So the attacker would need to steel this in a cross site scripting attack or similar. In addition, the installationId is created when the user authorizes you and then is redirected directly to your site, so the window of opportunity is however long it takes for the client to process the redirect. In addition your GithubApp will run into request limits, since you check the installationId on every install with a call to the github API (right?).

You could use OAuth, but you would need to communicate to the user, why you want to act on his/her behalf.

I’m imagining something like:

  1. On my site, Alice clicks “Install”, and is redirected to my GitHub App’s installation page with state=foo.
  2. On my site, Bob clicks “Install”, and is redirected to my GitHub App’s installation page with state=bar. Bob is a script.
  3. Bob, separately, obtains a recent installation_id and starts hitting my site’s Setup URL with state=foo&installation_id=... with a range of installation IDs.
  4. On GitHub, Alice installs my app, and is redirected to my Setup URL.
  5. On my site, one of Bob’s requests has Alice’s installation_id and it arrives just before Alice’s browser redirects to my site. Bob sends state=bar which we know is valid because we gave it to him earlier. My site now associates Bob with Alice’s installation.
  6. On my site, Alice is shown an error because the installation_id is already associated.
  7. While Alice figures out what’s happening, my site is acting on Alice’s account/organisation/repos/etc. on behalf of Bob.

Many things come out of this:

  • Bob doesn’t need Alice’s state value in order to forge installation_id because my site doesn’t have any way to know who that installation_id really belongs to.
  • Both Alice and Bob look like they’re trying to install an app, so my site can’t eliminate one or the other based on that.
  • Alice and Bob are both logged into my site, so I know who they are when they’re redirected back from GitHub; I don’t need state for that.
  • Hence the state value does not provide security. It’s really only useful for maintaining state across redirects to GitHub.
  • Bob may have to make 100s or 1000s of attempts before he is successful, but he’s a script so that doesn’t matter. Once successful, his subsequent attack can be scripted too, so that even if Alice immediately uninstalls the GitHub App from her account it may be too late.
  • We could mitigate Bob’s kind of attack by rate-limiting, say, or trying to detect malicious behaviour, but that’s imprecise and complex. Bob may be able to evade detection, and we may inadvertently penalise legitimate users.

The reason for using OAuth is to check that the installation_id unequivocally belongs to the user that is logged-in to my site. The chain of trust is:

  • User on my site (logged-in) →
  • User in GitHub (verified by OAuth) →
  • Installations (via GitHub API using user’s GitHub access token).

In the above scenario, we could check Bob’s installations and find that the forged installation_id is not among them.

I’m in the same boat here.

I assume we are a minority use case where we want to use server-to-server API, but we want to associate it externally to a user account on our side.

The only way I see so far is to use OAuth to solve this:

  1. During app installation, also authorize user and redirect to our service for linking
  2. If app is already installed but not linked, a flow where we perform oauth from my service and fetch all available installations from user for linking.

The interesting thing is I found my competitor services have this loophole and did not bother securing this.