frontdoor rule · FD015
GitHub OIDC trust based on repository_owner alone
Trust rests on an organization name
Severity: high.
What it means
The only constraint on the trust is repository_owner:
"StringEquals": { "token.actions.githubusercontent.com:repository_owner": "acme" }repository_owner is a display name, not a stable identifier. Names on
GitHub can be changed, and a name that is given up becomes available for anyone
to register.
Fires only when there is no sub condition and no repository_owner_id
condition. With either of those present, repository_owner is belt and braces
rather than the only thing holding the door.
What an attacker does
Two routes, both quiet:
- The org is renamed.
acmebecomesacme-corp, the old name is released, and an attacker registersacme. Every repository they create under it now satisfies your condition. - The org is deleted. Same outcome, without anyone at your company thinking about it at all.
There is a third case worth noting: a policy that globs the owner (acme*)
makes typosquatting work immediately, with no rename required.
How to fix it
Condition on the subject, which pins the repository as well as the owner:
"StringEquals": {
"token.actions.githubusercontent.com:aud": "sts.amazonaws.com",
"token.actions.githubusercontent.com:sub": "repo:acme/api:ref:refs/heads/main"
}And use the numeric owner id if you want an org-level condition at all. It cannot be re-registered:
"StringEquals": {
"token.actions.githubusercontent.com:repository_owner_id": "12345678"
}You can read the id from the token claims in a workflow run, or from
https://api.github.com/orgs/acme.
The same reasoning applies elsewhere: GitLab's namespace_path is a name,
namespace_id is not.
frontdoor answers this once, when you run it; Secorvia checks it continuously across every account you connect, on a free tier that needs no card.