Skip to content

A GitHub Enterprise remote is no longer asked about on github.com, so unowned-push keeps the allow-list's refusal for it - #296

Merged
HackingGate merged 4 commits into
mainfrom
fix-278-ghe-forge
Oct 3, 2026
Merged

HackingGate merged 4 commits into
mainfrom
fix-278-ghe-forge

Conversation

@HackingGate

Copy link
Copy Markdown
Owner

What

Forge::of_url now counts a host as GitHub only through git::is_github_host (moved from guard::names, unchanged): github.com, www.github.com, raw.githubusercontent.com, in any URL or ssh spelling. There is now one definition of "GitHub" in the binary, shared by private-names, the unowned-push guard and the shim's visibility lookup.

Why

Before this change, any host containing github counted as GitHub, so a remote on github.acme.com reached forge_owns. forge_owns runs gh api without --hostname, so the question went to github.com. An operator who administers github.com/acme/widget could push to github.acme.com/acme/widget past the allow-list. is_github_host already refused enterprise hosts for this reason.

Unknown forges (GHE included)

  • unowned-push / unowned-forge-target (hook seam): no gh call. The allow-list's answer stands, so an off-list destination is refused with exit 1. It does not pass because ownership could not be asked.
  • Shim public-target visibility lookup (git shim via origin): returns Silence::Refused("no forge client is known for that host...") instead of reading github.com's answer, so it is reported as could-not-tell.
  • A gh invocation is still classified by its command name. gh honours its own GH_HOST/--hostname, so this change does not touch that case.

Tests

  • the_forge_is_read_from_the_host_and_never_from_the_path: adds GHE https, scp-ssh and ssh:// port URLs plus notgithub.com, all None. The existing github.com controls still pass.
  • an_enterprise_remote_is_not_answered_for_by_github_com (tests/guard_cli.rs): a stub gh answers yes to everything, and a push to a GHE remote over https or ssh is still refused with exit 1 and no forge line.
  • docs/REFERENCE.md: the forge-ownership paragraph now says github.com only and covers enterprise hosts.

Closes #278

@coderabbitai

coderabbitai Bot commented Oct 3, 2026 •

Copy link
Copy Markdown

Warning

Review limit reached

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

Next included review available in 32 minutes.

Check out review usage here.

View limit details

Limit details: You’ve used the included review currently available.

Learn how review limits work.

Review configuration:

⚙️ Run configuration
  • Configuration used: defaults
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: cafb1462-0623-4776-abc5-4d3917a4e0cd
📥 Commits

Reviewing files that changed from the base of the PR and between d8c43eb and 72218c6.

📒 Files selected for processing (5)
  • docs/REFERENCE.md
  • src/git.rs
  • src/guard/names.rs
  • src/shim.rs
  • tests/guard_cli.rs
  • Autopilot · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@HackingGate HackingGate mentioned this pull request Oct 3, 2026
@codecov-commenter

codecov-commenter commented Oct 3, 2026 •

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 94.07%. Comparing base (d8c43eb) to head (72218c6).

Additional details and impacted files
@@            Coverage Diff             @@
##             main     #296      +/-   ##
==========================================
+ Coverage   94.04%   94.07%   +0.02%     
==========================================
  Files          46       46              
  Lines       20868    20962      +94     
==========================================
+ Hits        19626    19720      +94     
  Misses       1242     1242              

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

… unowned-push keeps the allow-list's refusal for it

Forge::of_url counted any host containing "github" as GitHub, so a remote
on github.acme.com reached forge_owns, whose `gh api` carries no
--hostname and answered about the same-named github.com repository. An
operator who administers github.com/acme/widget could push to
github.acme.com/acme/widget past the allow-list. The git shim's
visibility lookup read the same classifier.

There is now one definition of GitHub, git::is_github_host (moved from
guard::names), and Forge::of_url uses it. An enterprise host is a host no
client answers for: unowned-push refuses it on the allow-list alone (exit
1, no gh call), and the shim's visibility lookup reports there was nobody
to ask instead of reading github.com's answer.

Closes #278
… longer makes github.com answer for a push to another host

`git::host` ended a url's authority only at `/`. In
`https://evil.com#@github.com/acme/widget.git` it therefore stripped
`evil.com#` as userinfo and read the host as github.com, and
`owner_repo` read `acme/widget` -- while git and curl connect to
evil.com. A pusher who administers github.com/acme/widget had the
unowned-push guard's forge question answered yes, and the off-list push
to evil.com passed.

The authority now ends at the first `/`, `?` or `#`, as RFC 3986 reads
it, before userinfo and port are stripped. `owner_repo` reads a
`scheme://` url's owner and repository from that same path, without
query or fragment, so the two never disagree about one url: the
`#@`/`?@` forms name host evil.com, no forge, and no owner/repo, and the
push is refused for an unreadable destination.

Unit rows cover the host, forge and owner/repo of both forms; an
end-to-end case pushes to them with a stub `gh` that says yes and
expects exit 1. All fail against the previous parsing.
…ost, or a file:// scheme names no host and no repository, so unowned-push refuses it as unreadable

Three spellings still let `git::host` and `git::owner_repo` read
github.com's acme/widget while git contacts another host, and a stub
`gh` that said yes passed the push:

- `ssh://evil.com%2F@github.com/acme/widget.git` (and `git+ssh://`,
  `git://`): git url-decodes the authority before splitting it, so the
  user ends at the decoded `/` and ssh connects to evil.com.
- `[github.com:x@evil.com]:acme/widget.git`: git strips the brackets of
  a scp-like host and ssh connects to evil.com as user x.
- `file://github.com/acme/widget.git`: a file:// url names no host, yet
  github.com was read from it.

Rather than a second copy of git's parser, one predicate, `ambiguous`,
fails closed: a `scheme://` authority containing `%`, any `file://` url,
and a scp-like host part containing `[` yield None from both functions,
and the push is refused for an unreadable destination. A percent-encoded
credential in a url is refused with them.

This corrects the previous commit's claim that `host` and `owner_repo`
"never disagree": the contract, now written on `host`, is that they
agree about a url or both yield None when it is ambiguous.

Unit rows cover host, owner/repo and forge for each form, with
github.com controls; one end-to-end case per form pushes with a stub
`gh` that says yes and expects exit 1. All fail with `ambiguous`
disabled.
…itory, so a push to an allowed bare repository by file:// passes again

bddacc4 refused every file:// url as unreadable, so a push to
file:///tmp/x/bare.git with allowed_repos = ["x/bare"] exited 1 where the
same path spelled plainly passed, and the visibility guard, names and the
shim lost origin's owner and repository the same way. host() now returns
None for file:// and owner_repo() reads its path like a plain path, so
file://github.com/... asks no forge and is judged by the allow-list alone.
HackingGate added a commit that referenced this pull request Oct 3, 2026
Three engine changes since 1.24.0. A rule whose `files.include` root is not
on disk no longer selects nothing, prints one line on stderr and passes: the
scan exits 2 naming the rule, the root and that the root does not exist. This
holds for every rule, bundled sets included, so a repository that inherits
`default-token-grant` and has no `.github/workflows` is now refused (#294).

The relative-link check removes inline code spans from each line before it
reads links, delimited as CommonMark does: a run of N backticks closes at the
next run of exactly N, and an unmatched run stays literal. A regular
expression in backticks such as `[2-9](\.\d+)` is no longer reported as a
link to a missing file (#295).

A remote counts as GitHub only when its host is github.com, www.github.com or
raw.githubusercontent.com, the one definition private-names and the shim
already used. A GitHub Enterprise remote is no longer asked about on
github.com: unowned-push keeps the allow-list's refusal for it with exit 1,
and the shim's visibility lookup reports could-not-tell. Any other unknown
forge is refused as before (#296).

encoding_rs is 0.8.42 (#281), and CI runs astral-sh/setup-uv 10.2.0 (#280).

A consumer taking the pin to v1.25.0 must have every `files.include` root its
rules name on disk, inherited sets included: a missing root now fails the
scan with exit 2 where it warned, and the fix is to correct the root or remove
it. A consumer pushing to a GitHub Enterprise remote that its allow-list does
not name is now refused where github.com may have answered for it.
@HackingGate
HackingGate merged commit 947bab9 into main Oct 3, 2026
12 of 13 checks passed
@HackingGate
HackingGate deleted the fix-278-ghe-forge branch October 3, 2026 09:56
HackingGate added a commit that referenced this pull request Oct 3, 2026
Three engine changes since 1.24.0. A rule whose `files.include` root is not
on disk no longer selects nothing, prints one line on stderr and passes: the
scan exits 2 naming the rule, the root and that the root does not exist. This
holds for every rule, bundled sets included, so a repository that inherits
`default-token-grant` and has no `.github/workflows` is now refused (#294).

The relative-link check removes inline code spans from each line before it
reads links, delimited as CommonMark does: a run of N backticks closes at the
next run of exactly N, and an unmatched run stays literal. A regular
expression in backticks such as `[2-9](\.\d+)` is no longer reported as a
link to a missing file (#295).

A remote counts as GitHub only when its host is github.com, www.github.com or
raw.githubusercontent.com, the one definition private-names and the shim
already used. A GitHub Enterprise remote is no longer asked about on
github.com: unowned-push keeps the allow-list's refusal for it with exit 1,
and the shim's visibility lookup reports could-not-tell. Any other unknown
forge is refused as before (#296).

encoding_rs is 0.8.42 (#281), and CI runs astral-sh/setup-uv 10.2.0 (#280).

A consumer taking the pin to v1.25.0 must have every `files.include` root its
rules name on disk, inherited sets included: a missing root now fails the
scan with exit 2 where it warned, and the fix is to correct the root or remove
it. A consumer pushing to a GitHub Enterprise remote that its allow-list does
not name is now refused where github.com may have answered for it.
HackingGate added a commit that referenced this pull request Oct 3, 2026
Three engine changes since 1.24.0. A rule whose `files.include` root is not
on disk no longer selects nothing, prints one line on stderr and passes: the
scan exits 2 naming the rule, the root and that the root does not exist. This
holds for every rule, bundled sets included, so a repository that inherits
`default-token-grant` and has no `.github/workflows` is now refused (#294).

The relative-link check removes inline code spans from each line before it
reads links, delimited as CommonMark does: a run of N backticks closes at the
next run of exactly N, and an unmatched run stays literal. A regular
expression in backticks such as `[2-9](\.\d+)` is no longer reported as a
link to a missing file (#295).

A remote counts as GitHub only when its host is github.com, www.github.com or
raw.githubusercontent.com, the one definition private-names and the shim
already used. A GitHub Enterprise remote is no longer asked about on
github.com: unowned-push keeps the allow-list's refusal for it with exit 1,
and the shim's visibility lookup reports could-not-tell. Any other unknown
forge is refused as before. A remote URL's host now ends at the first `/`, `?`
or `#`, and userinfo is stripped up to the last `@`, so
`https://evil.com#@github.com/acme/widget.git` is read as evil.com, which is
where git pushes, and unowned-push no longer accepts it on the strength of
github.com ownership. Two spellings uphold cannot read the way git does now
name no host and no repository, so unowned-push refuses them as an unreadable
destination: a `%` anywhere in a `scheme://` URL's authority and a `[` in a
scp-like host. A `file://` URL names no host, so it is never asked about on a
forge, and its repository is read from the path like a plain local path, so
the allow-list judges it by its path (#296).

encoding_rs is 0.8.42 (#281), and CI runs astral-sh/setup-uv 10.2.0 (#280).

A consumer taking the pin to v1.25.0 must have every `files.include` root its
rules name on disk, inherited sets included: a missing root now fails the scan
with exit 2 where it warned, and the fix is to correct the root or remove it.
A consumer pushing to a GitHub Enterprise remote that its allow-list does not
name is now refused where github.com may have answered for it. An ssh Host
alias such as `git@github-work:me/repo`, or `ssh.github.com:443`, is no longer
counted as GitHub, so a push through one to a destination the allow-list does
not name is refused; name the destination in the allow-list, or use a
github.com URL. A push through a remote URL carrying a percent-encoded
credential is refused whatever the allow-list names; drop the encoded
credential from the URL.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

A GitHub Enterprise remote is asked about on github.com, so unowned-push can pass a push on a same-named github.com repository

2 participants