frontdoor rule · FD015

GitHub OIDC trust based on repository_owner alone

Trust rests on an organization name

Amazon Web ServicesUpdated Source on GitHub

Severity: high.

What it means

The only constraint on the trust is repository_owner:

jsonc
"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. acme becomes acme-corp, the old name is released, and an attacker registers acme. 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:

jsonc
"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:

jsonc
"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.