SAML SSO in GitHub Enterprise does not prevent conveyance of authentication as organization member when no SAML session is established. GitHub should be avoided as an identity provider if you’re using applications with GitHub SSO and are relying on SAML for device posture. Some of these applications — particularly code quality and CI tools — may also provide indirect access to resources meant to be protected by GitHub SAML SSO retrieved with a non-user token to authenticated organization members.
GitHub considers this a non-issue.
Tailscale, one of the vendors I have initially discovered and confirmed the issue at, is publishing a security bulletin simultaneously to this post.
The security model of GitHub Enterprise — and SAML sessions specifically — likely differs from your preconceptions. The issue described here is not per se a bug, but seeing how the scope of the protection with SAML is not documented, and the behavior is not consistent with other security features outside the enterprise scope, it is likely that many enterprise organizations and GitHub App implementers are working off the incorrect assumption that they’re safer than they actually are.
GitHub OAuth Application Policy 🔗
To explain why this issue went undetected, and to demonstrate how the SAML implementation differs from the remainder of the GitHub Security model, we have to take a look at how other parts of GitHub handle organization visibility: The organization-level OAuth Application Policy feature.
Enabled by default since 2014, this feature will prevent OAuth applications authorized by any member of your organization to gain access to organization resources. During the consent flow, a user can request the application to be allowed inside the organization, which sends an email to organization administrators for approval. Only then can an OAuth app even gain knowledge of a user’s organization membership — without approval, the API call that list the user’s organizations will exclude organizations where the app is not approved.
This exclusion makes sense, because communicating membership to an organization may be used as authentication information. This design choice shows that GitHub understands this.
How SAML SSO works with GitHub 🔗
With GitHub’s enterprise cloud offering, a lot of emphasis is put on the ability of users to use their existing personal GitHub account for work. The ability to “layer” your work context on top of your existing GitHub context on a work machine, while still being able to use the same account for personal projects and open source work is likely the biggest selling point of Enterprise Cloud.
GitHub achieves this by letting you configure organizations or entire enterprises as SAML consumers. This way, after logging in, you will only have as much access to the enterprise organizations as any other user on GitHub would have.
Whenever you want to access repositories, you’ll need to go through the login flow of your company’s identity provider. This provider can not just check that your account still exists, they might also enforce device posture — making sure you’re using your account from a properly set up work laptop. Whatever that might be in your organization.
There is a similar flow to enable personal access tokens and SSH keys to be used with the protected organization.
SAML and OAuth 🔗
Putting the OAuth Application Policy aside, how does OAuth work with SAML SSO sessions?
Looking at the precedent above, my assumption was that unless a SAML SSO session exists, the flow should ask for one to be established in the same way and the behavior is analogue to the OAuth Application Policy, and if a session is not established the organization should be hidden from the application’s view.
As expected, a new step is added to the OAuth flow, where you’re asked to start a SAML SSO session.
However, if an application requests the
org:read scope to list the user’s organizations, it does not actually matter if you click “Authorize” or “Continue”. The organization is included in the scope of the token either way, conveying authentication as organization member.
In the GitHub security model, SAML will prevent access to your organization via OAuth flow, but it will not prevent authenticating as a member of it.
This seems to contradict the documentation for the endpoint.
This only lists organizations that your authorization allows you to operate on in some way.
Isn’t this what SAML is supposed to prevent?
About OAuth Apps, GitHub Apps, and SAML SSO
You must have an active SAML session each time you authorize an OAuth App or GitHub App to access an organization that uses or enforces SAML SSO.
I honestly can’t tell. I did not find a more specific feature description for SAML SSO.
I’m not interested in splitting hairs about this being a bug or simply undocumented behavior. It seems to be outside of the expectations of OAuth app developers and enterprise administrators alike.
Additionally, many applications, especially code analysis and CI systems, use server-to-server tokens to fetch and analyze code (and are thus not subject to SAML), but grant access to that code within their own application with nothing more than a
read:org scope. This represents complete circumvention of SAML as a mechanism to safeguard source code and other organization resources — and you can’t really blame the application developers for not knowing about the exact mechanics of GitHub SAML SSO.
For Enterprises, ensure that OAuth Application Policy is on. While this does not protect you from users signing in to approved applications from home, it does prevent a user from communicating their organization membership to unapproved applications.
If you use GitHub for SSO and need SAML, migrate away or urge your vendors to implement the OAuth consumer remedy below, unless GitHub changes this behavior in the future.
All GitHub Apps that fetch code and also allow a user to sign in to their service should be tested and the impact of your users being able to access them from home should be assessed.
For OAuth consumers, GitHub Enterprise Support suggested applications could call the membership API, which would return 403 if SAML is enabled, but no session exists. They did not say whether this is considered best practice. Because there is no way to start an OAuth flow specifically asking for an organization to be included, this workaround has considerable UX issues. Implementers would need to show they recognize an organization membership is known to them, but explain to the user that they need to SAML authorize it for access in a restarted OAuth flow.
GitHub’s response 🔗
I need to admit that I made the mistake of initially reporting this issue via GitHub Enterprise Support. I gained some valuable insight I otherwise wouldn’t have — like the membership API returning 403 when no session exists. Ultimately it was still the wrong place to report security issues.
I did eventually submit the issue to HackerOne, and received this response a day layer.
Thanks for the submission!
We have reviewed your report and determined that it is known behavior and is working as intended. Once authorized, OAuth applications do not require an SSO session. If desired, this may be mitigated by whitelisting which apps are allowed to access the org.
Additionally, granting an OAuth app the
org:readscope is intended to provide the ability to read organization membership:
read:orgRead-only access to organization membership, organization projects, and team membership.
Best regards and happy hacking!
Suffice it to say I’m quite disappointed GitHub considers it a non-issue that the core feature of their Enterprise offer (”designed for teams who want advanced authentication”) does not protect to the extent their customers expect.
I only reported this issue to vendors that I used to initially reproduce and confirm the issue. None of them consider this issue harmless or known behavior, all are working on or have deployed a fix.
Unfortunately this issue affects so many vendors, that it’d be close to impossible for me to test and inform all of them in private without it turning into a full time unpaid job. That would be GitHub’s responsibility. But they do not seem to care.
Disclosure Timeline 🔗
- The issue was reported to GitHub Enterprise Support.
- After some initial communication issues, GitHub Enterprise Support requests a video reproducing the issue. It is provided the same day.
- GitHub Enterprise Support replies with a breakdown of API calls related to the reproduction; suggests I take the issue to Tailscale, who I’ve used in the video reproduction. I restate the possible impact and ask for documentation to be amended.
- A copy of the API breakdown and additional details is sent to [email protected]. It is acknowledged within 2 hours.
- GitHub Enterprise Support agrees the current state of the documentation is suboptimal, but seemingly does not consider it a security issue.
- Tailscale asks for clarification, which I provided on the same day.
- A previous version of this document finds its way to GitHub SIRT (Security Incident Response Team). I did not receive any feedback from them.
- I request an update from Tailscale and share a previous version of this document.
- Tailscale confirms they’re taking steps to mitigate the issue.
- Tailscale confirms their fix is deployed.
- I finally submit this issue to GitHub’s HackerOne after other channels fail me.
- In a response on HackerOne, GitHub considers this issue “known behavior and working as intended”. I am restating the impact, but receive no response.
- This post is published.