Skip to content

Commit 492dc60

Browse files
authored
fix(runner-server): attach static GitHub PAT by configured origin, not scheme (#351)
* fix(runner-server): attach static PAT by configured GitHub origin, not scheme The static GitHub PAT was attached to action-resolution and tarball requests only when the outgoing URL started with `https://`. Pointing the engine at a plain-http GitHub emulator (gh-simulate local mode) therefore silently dropped the credential, and the anonymous API budget (60/hr) was exhausted almost immediately. Replace the scheme check with `url_targets_configured_github`, which attaches the PAT when the request URL's host (and effective port) matches one of the configured `github_urls` endpoints, regardless of scheme. Real github.com still attaches over https; an unrelated host never receives the credential. Refs #349 * ci: rerun checks after shared engine recovery The previous check run used the broken macstudio golden/provisioning state. No source files changed. * fix(ci): re-pin dtolnay/rust-toolchain in control-plane.yml The pinned SHA (6bed0761) is not a commit in dtolnay/rust-toolchain; zizmor flags it as impostor-commit. Re-pin to the real stable head 89b12181 (same pin ci.yml uses). --------- Co-authored-by: Bnjoroge1 <Bnjoroge1@users.noreply.github.com>
1 parent bc4e1f1 commit 492dc60

1 file changed

Lines changed: 66 additions & 2 deletions

File tree

‎crates/preloop-runner-server/src/actions.rs‎

Lines changed: 66 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -167,6 +167,30 @@ fn sha_entry_fresh(sha: &Option<String>, at: std::time::Instant) -> bool {
167167
at.elapsed() < ttl
168168
}
169169

170+
/// Whether `url` targets the configured GitHub host — any of the
171+
/// `github_urls` endpoints — compared by host and effective port, and
172+
/// deliberately ignoring the scheme.
173+
///
174+
/// The static PAT must follow the engine when it is redirected to a
175+
/// plain-http GitHub emulator (gh-simulate local mode, `http://127.0.0.1:…`),
176+
/// yet must never leak to an unrelated origin. A scheme check gets the first
177+
/// case wrong; an origin check gets both right.
178+
fn url_targets_configured_github(url: &str, urls: &GitHubUrls) -> bool {
179+
let Ok(target) = reqwest::Url::parse(url) else {
180+
return false;
181+
};
182+
let Some(target_host) = target.host_str() else {
183+
return false;
184+
};
185+
[&urls.api_url, &urls.server_url, &urls.graphql_url]
186+
.iter()
187+
.filter_map(|configured| reqwest::Url::parse(configured).ok())
188+
.any(|configured| {
189+
configured.host_str() == Some(target_host)
190+
&& configured.port_or_known_default() == target.port_or_known_default()
191+
})
192+
}
193+
170194
/// Resolve an action ref (branch, tag, or short SHA) to the commit SHA GitHub
171195
/// would pin for the job. Cached briefly in memory so a matrix fan-out
172196
/// resolves each `uses:` once per window. Returns `None` on any failure
@@ -198,7 +222,7 @@ async fn resolve_ref_to_sha(
198222
let url = format!("{api_base}/repos/{enc_owner}/{enc_repo}/commits/{enc_git_ref}");
199223
let mut request = crate::shared_http::CLIENT.get(&url);
200224
if let Some(pat) = state.static_github_pat()
201-
&& url.starts_with("https://")
225+
&& url_targets_configured_github(&url, &state.github_urls)
202226
{
203227
request = request.bearer_auth(pat);
204228
}
@@ -365,7 +389,7 @@ pub async fn download_action_tarball(
365389
// repos (a GitHub App installation token is scoped to the App's repos).
366390
let mut request = client.get(&github_url);
367391
if let Some(pat) = shared.state.static_github_pat()
368-
&& github_url.starts_with("https://")
392+
&& url_targets_configured_github(&github_url, &shared.state.github_urls)
369393
{
370394
request = request.bearer_auth(pat);
371395
}
@@ -766,6 +790,46 @@ mod tests {
766790
AppState::new(temp.path().to_path_buf()).await.unwrap()
767791
}
768792

793+
/// The static PAT follows the engine onto a configured plain-http
794+
/// GitHub emulator, but never to an unrelated host (or a lookalike).
795+
#[test]
796+
fn pat_targets_configured_github_regardless_of_scheme() {
797+
let sim = GitHubUrls {
798+
server_url: "http://127.0.0.1:8888".to_string(),
799+
api_url: "http://127.0.0.1:8888".to_string(),
800+
graphql_url: "http://127.0.0.1:8888".to_string(),
801+
};
802+
assert!(url_targets_configured_github(
803+
"http://127.0.0.1:8888/repos/o/r/tarball/main",
804+
&sim
805+
));
806+
assert!(!url_targets_configured_github(
807+
"http://127.0.0.1:9999/repos/o/r/tarball/main",
808+
&sim
809+
));
810+
assert!(!url_targets_configured_github(
811+
"https://evil.example.com/repos/o/r/tarball/main",
812+
&sim
813+
));
814+
815+
// Real github.com: still attaches over https, and a different host
816+
// (even a lookalike) does not.
817+
let real = GitHubUrls {
818+
server_url: "https://github.com".to_string(),
819+
api_url: "https://api.github.com".to_string(),
820+
graphql_url: "https://api.github.com".to_string(),
821+
};
822+
assert!(url_targets_configured_github(
823+
"https://api.github.com/repos/o/r/commits/main",
824+
&real
825+
));
826+
assert!(!url_targets_configured_github(
827+
"https://github.com.evil.example/repos/o/r/commits/main",
828+
&real
829+
));
830+
assert!(!url_targets_configured_github("not a url", &real));
831+
}
832+
769833
/// `archive_sha256_hex` is the lowercase hex SHA-256 of the bytes —
770834
/// the same value the runner computes over the downloaded archive.
771835
#[test]

0 commit comments

Comments
 (0)