ghtier

Strange findings

Places where GitHub access works differently from what most people expect. Each one quotes the GitHub docs or changelog that states it, links the permissions, roles and scopes involved, and says how to check for it and how to close it.

Also as JSON: /data/findings.json

  1. Removing someone in the IdP does not stop their tokens

    Enterprise Cloud · added 2026-10-11

    What people expect

    When a person is removed or disabled in the identity provider, they lose access to the organization.

    What GitHub does

    With SAML SSO and no SCIM, removing a user in the IdP does not remove them from the organization. Their SSO session expires, but personal access tokens, SSH keys and OAuth tokens they already authorized for SSO keep working until an owner removes the member or revokes the authorizations. With SCIM the member is removed automatically. With Enterprise Managed Users the account is suspended and its tokens and keys are deleted.

    How to check

    Compare the organization's SSO credential authorizations with the people who are still active in the IdP. Anyone who is gone from the IdP but still listed can use those credentials.

    gh api /orgs/ORG/credential-authorizations --paginate \
      --jq '.[] | [.login, .credential_type, .credential_authorized_at, .credential_accessed_at] | @tsv'

    How to close it

    Turn on SCIM provisioning so removal in the IdP removes the member. Until then, make removing the GitHub member and revoking their credential authorizations part of offboarding (DELETE /orgs/{org}/credential-authorizations/{credential_id}).

  2. The repo scope can change organization settings

    Every setup · added 2026-10-11

    What people expect

    A classic token needs admin:org to change organization settings; repo is about repositories.

    What GitHub does

    GitHub's REST docs accept admin:org or repo for PATCH /orgs/{org}, and list repo for writing organization Actions secrets and creating runner registration tokens. A classic token with only repo, created by an organization owner, can change the organization's settings, including the base repository permission for every member, without ever asking for admin:org.

    How to check

    Find organization owners who use classic tokens. In an organization with SAML SSO, the credential authorizations list shows each token's scopes.

    gh api /orgs/ORG/credential-authorizations --paginate \
      --jq '.[] | select(.scopes | index("repo")) | [.login, .credential_authorized_at] | @tsv'

    How to close it

    Have owners use fine-grained tokens or a separate account for automation, and restrict classic tokens in the organization's personal access token policy.

  3. Write access reads every repository secret

    Every setup · added 2026-10-11

    What people expect

    Secrets are hidden. People who can push code cannot see them.

    What GitHub does

    GitHub states that any user with write access to a repository has read access to all secrets configured in it: they can push a workflow on a branch that prints or sends them. Environment secrets are only given to jobs that use the environment, and an environment with required reviewers holds the job until someone approves it.

    How to check

    List the repository's secrets and who has write access. Every person and token with write can read each of those secrets.

    gh api /repos/ORG/REPO/actions/secrets --jq '.secrets[].name'
    gh api /repos/ORG/REPO/collaborators --paginate \
      --jq '.[] | select(.permissions.push) | .login'

    How to close it

    Move deployment secrets into environments with required reviewers, or replace them with OIDC so there is no stored secret to read.

  4. Workflow files can be pushed without the workflow scope

    Every setup · added 2026-10-11

    What people expect

    A classic token without the workflow scope cannot add or change workflow files.

    What GitHub does

    GitHub's scope docs say a workflow file can be committed without the workflow scope when the same file, with the same path and contents, already exists on another branch of the repository. A workflow that sits on any branch can be copied to another branch with a token that only has repo.

    How to check

    Look for workflow files on branches other than the default branch, especially ones that use secrets or pull_request_target.

    How to close it

    Protect .github/workflows with a push ruleset or code owner review, and delete stale branches that carry workflow files.

  5. Older organizations still give GITHUB_TOKEN write access

    Every setup · added 2026-10-11

    What people expect

    GITHUB_TOKEN is read-only unless a workflow asks for more.

    What GitHub does

    Read-only became the default on 2 February 2023, but only for new enterprises, organizations and repositories. Anything created earlier kept read and write for every workflow without a permissions block.

    How to check

    Read the organization's and each repository's default workflow permissions.

    gh api /orgs/ORG/actions/permissions/workflow
    gh api /repos/ORG/REPO/actions/permissions/workflow

    How to close it

    Set the default to read at the enterprise or organization level, and give each workflow a permissions block with what it needs.

  6. GITHUB_TOKEN may approve pull requests in older repositories

    Every setup · added 2026-10-11

    What people expect

    A workflow cannot approve its own pull request, so required reviews always mean a person looked.

    What GitHub does

    The setting that lets Actions approve pull requests arrived in January 2022 turned on, so existing workflows kept working; it was later renamed "Allow GitHub Actions to create and approve pull requests". Only new personal repositories are documented as having it off. Where it is on, a workflow with pull-requests: write can approve a pull request and count towards required reviews.

    How to check

    Check can_approve_pull_request_reviews for the organization and its repositories.

    gh api /orgs/ORG/actions/permissions/workflow --jq .can_approve_pull_request_reviews

    How to close it

    Turn the setting off at the enterprise or organization level unless a workflow needs it.

  7. Branch protection does not apply to admins by default

    Every setup · added 2026-10-11

    What people expect

    A protected branch requires a review from everyone.

    What GitHub does

    By default a branch protection rule does not apply to people with admin on the repository, or to custom roles with "bypass branch protections". Rulesets have an explicit bypass list instead.

    How to check

    Check whether each protected branch also enforces its rules for admins.

    gh api /repos/ORG/REPO/branches/main/protection --jq .enforce_admins.enabled

    How to close it

    Turn on "Do not allow bypassing the above settings", or move to rulesets with a short, named bypass list.

  8. An exempt ruleset bypass leaves no audit entry

    Every setup · added 2026-10-11

    What people expect

    Every time someone bypasses a ruleset, it is logged.

    What GitHub does

    A bypass actor with bypass_mode exempt is not checked against the rules at all, and GitHub creates no bypass audit entry for it.

    How to check

    Look for exempt bypass actors in organization and repository rulesets.

    for id in $(gh api /orgs/ORG/rulesets --jq '.[].id'); do
      gh api /orgs/ORG/rulesets/$id --jq '.name as $n | .bypass_actors[] | select(.bypass_mode == "exempt") | [$n, .actor_type, .actor_id] | @tsv'
    done

    How to close it

    Use the always or pull_request bypass modes, which are logged, unless the actor is an automation you trust not to need review.

  9. Deploy keys keep working after the person leaves

    Every setup · added 2026-10-11

    What people expect

    Removing someone from the organization removes the access they set up.

    What GitHub does

    A deploy key belongs to the repository, not to the person who added it. It stays active when that person is removed, and it never expires.

    How to check

    List deploy keys with write access and ask who holds each private key today.

    gh api /repos/ORG/REPO/keys --jq '.[] | select(.read_only == false) | [.id, .title, .created_at] | @tsv'

    How to close it

    Rotate deploy keys when their owner leaves, prefer read-only keys, and use a GitHub App where write access is needed.

  10. A GitHub App stays installed when its installer leaves

    Every setup · added 2026-10-11

    What people expect

    Integrations someone added go away with them.

    What GitHub does

    A GitHub App stays installed if the person who set it up leaves the organization. Its private keys do not expire either; they must be revoked by hand, and anyone holding one can mint installation tokens.

    How to check

    List the organization's installations and confirm an owner for each, and the age of each App's private keys.

    gh api /orgs/ORG/installations --paginate \
      --jq '.installations[] | [.app_slug, .created_at, .repository_selection] | @tsv'

    How to close it

    Give every App a named owner, remove unused installations, and rotate private keys on a schedule or keep them in a KMS.

  11. Classic tokens only die after a year unused

    Every setup · added 2026-10-11

    What people expect

    Tokens expire.

    What GitHub does

    Classic personal access tokens have no expiration requirement and OAuth app tokens are long-lived by default. GitHub revokes them automatically only when they have not been used for a year, so a token used now and then lives on.

    How to check

    In an organization with SAML SSO, the credential authorizations list shows each token's expiry and last use.

    gh api /orgs/ORG/credential-authorizations --paginate \
      --jq '.[] | select(.credential_type == "personal access token") | [.login, .authorized_credential_expires_at // "never", .credential_accessed_at] | @tsv'

    How to close it

    Restrict classic tokens in the organization's personal access token policy and use fine-grained tokens or GitHub Apps, which do expire.

  12. A fine-grained token reads every public repository

    Every setup · added 2026-10-11

    What people expect

    A fine-grained token can only reach the repositories selected for it.

    What GitHub does

    Every fine-grained token also has read-only access to all public repositories on GitHub, whatever repositories are selected.

    How to check

    Nothing to check per token; treat public repositories as readable by any token.

    How to close it

    Keep anything sensitive out of public repositories. The repository selection only limits private and internal ones.

  13. Deleting a fork does not delete its commits

    Every setup · added 2026-10-11

    What people expect

    Deleting a fork removes what was pushed to it.

    What GitHub does

    Git data from any repository in a fork network can be read from any other repository in the same network, including the upstream, even after a fork is deleted. A commit pushed to a fork stays reachable by its hash.

    How to check

    Assume anything ever pushed to any fork of a public repository is still readable.

    How to close it

    Rotate any secret that was pushed to a fork. Deleting the fork or the branch does not remove it.

  14. A push ruleset bypass reaches every fork

    Team, Enterprise Cloud, EMU, Server · added 2026-10-12

    What people expect

    A bypass on a repository's push ruleset only applies to that repository, and each fork's admins decide their own.

    What GitHub does

    Push rulesets cover a private or internal repository and its entire fork network, and so does their bypass list: only those who can bypass the ruleset in the root repository can bypass it in any fork. A bypass actor added for the root can push blocked files to every fork, and a fork's own admins cannot bypass it.

    How to check

    List the bypass actors on push rulesets for private and internal repositories that have forks.

    gh api /repos/ORG/REPO/rulesets --jq '.[] | select(.target == "push") | .id' |
      xargs -I{} gh api /repos/ORG/REPO/rulesets/{} --jq '[.name, (.bypass_actors[] | "\(.actor_type) \(.actor_id) \(.bypass_mode)")] | @tsv'

    How to close it

    Keep push ruleset bypass lists empty or to a break-glass team, since each entry applies across the fork network.

  15. Pull requests a workflow opens need a person to start their checks

    Every setup · added 2026-10-12

    What people expect

    A pull request opened by a workflow with GITHUB_TOKEN either runs its checks like any other, or runs nothing at all.

    What GitHub does

    Events made with GITHUB_TOKEN do not start workflow runs, with exceptions. A pull request it opens or updates does start runs, but they wait for approval, and someone with write access has to approve them from the merge box. GitHub's suggested way around the wait is a GitHub App or personal access token, whose pull requests run without approval.

    How to check

    Find workflows that open or update pull requests, and see which token they use.

    How to close it

    Read the pull request before approving its runs. Where a bot needs runs without approval, use a GitHub App with the fewest permissions, not a personal access token, and keep its pull requests behind required reviews.