Skip to content

feat: add builder pattern for customizable GitHub API URL#36

Merged
jdx merged 2 commits intomainfrom
feat/custom-github-api-url
Apr 4, 2026
Merged

feat: add builder pattern for customizable GitHub API URL#36
jdx merged 2 commits intomainfrom
feat/custom-github-api-url

Conversation

@jdx
Copy link
Copy Markdown
Owner

@jdx jdx commented Apr 4, 2026

Summary

  • Adds AttestationClientBuilder with a builder pattern for constructing AttestationClient
  • Supports setting a custom base_url to override the default https://api.github.com, enabling use behind reverse proxies
  • Fixes github_headers to send auth tokens and API version headers to the configured base URL (not just api.github.com)

Usage

let client = AttestationClient::builder()
    .base_url("https://my-proxy.internal/github")
    .github_token("ghp_xxx")
    .build()?;

The existing AttestationClient::new() API is unchanged.

Closes #32

Test plan

  • cargo check passes
  • All existing tests pass

🤖 Generated with Claude Code


Note

Medium Risk
Medium risk because it changes how auth and API-version headers are conditionally attached to outbound HTTP requests; mistakes could break attestation fetching or leak tokens to unintended endpoints.

Overview
Adds an AttestationClientBuilder to construct AttestationClient with an optional custom base_url (defaulting to https://api.github.com) and optional GitHub token, while keeping AttestationClient::new() by delegating to the builder.

Updates github_headers to apply Authorization and x-github-api-version headers based on whether a request URL matches the configured base_url (instead of hard-coding api.github.com), preventing headers from being sent to non-matching hosts (e.g., external bundle_urls).

Re-exports AttestationClientBuilder from lib.rs and adds mockito tests covering default/custom base URLs, trailing-slash normalization, header injection for custom hosts, and ensuring headers are not sent to different hosts.

Reviewed by Cursor Bugbot for commit 1406408. Bugbot is set up for automated code reviews on this repo. Configure here.

Allow customizing the GitHub API URL via `AttestationClient::builder().base_url(...)`,
enabling use behind reverse proxies or custom GitHub API endpoints.

Closes #32

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Copy link
Copy Markdown
Contributor

@gemini-code-assist gemini-code-assist Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces a builder pattern for the AttestationClient, allowing for configurable base URLs and GitHub tokens, and re-exports the new builder. A security vulnerability was identified in the URL validation logic within the github_headers method, where the use of starts_with could lead to credential leakage via domain shadowing. A suggestion was provided to ensure the URL match occurs at a proper path boundary.

Comment thread src/api.rs Outdated
.map_err(|e| AttestationError::Api(e.to_string()))?,
);
}
if url.starts_with(&self.base_url) {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

security-high high

The use of starts_with for URL validation is insecure as it can lead to credential leakage via domain shadowing. For example, if base_url is https://api.github.com, it would match https://api.github.com.attacker.com, sending the AUTHORIZATION header to an untrusted domain.

You should ensure that the match occurs at a path boundary (i.e., the URL is either exactly the base URL or starts with the base URL followed by a /).

Suggested change
if url.starts_with(&self.base_url) {
if url == self.base_url || url.starts_with(&format!("{}/", self.base_url)) {

Copy link
Copy Markdown

@cursor cursor Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 34ac4e5. Configure here.

Comment thread src/api.rs Outdated
@greptile-apps
Copy link
Copy Markdown

greptile-apps Bot commented Apr 4, 2026

Greptile Summary

This PR introduces AttestationClientBuilder so callers can configure a custom base_url (useful behind reverse proxies) and an optional GitHub token while keeping AttestationClient::new() unchanged via delegation to the builder. The github_headers method is reworked to scope Authorization and x-github-api-version headers only to requests whose URL begins with the configured origin, preventing them from leaking to external bundle-download hosts.

  • The subdomain-spoofing gap from the prior review (bare starts_with without a trailing separator) is correctly fixed: appending "/" to form base_with_slash means https://api.github.com.evil.com/… no longer passes the prefix check.
  • New mockito integration tests cover default URL, custom URL, trailing-slash normalisation, builder delegation, and — critically — asserting that auth headers are absent when bundles are fetched from a different origin.
  • AttestationClient and AttestationClientBuilder both derive Debug while holding github_token: Option<String> as an unredacted field; the raw token will appear in any debug-formatted output or panic backtrace.
  • base_url() performs no URL validation; a caller who passes a URL containing a query string will produce a base_with_slash that never prefix-matches any real request URL, causing auth headers to be silently omitted with no error.

Confidence Score: 5/5

Safe to merge; no P0/P1 issues remain — the prior token-leakage vulnerability is fixed and both remaining findings are P2 hardening suggestions.

The primary security concern from the previous review (unbounded starts_with allowing subdomain-spoofed bundle URLs to receive auth headers) is correctly addressed by appending '/' before the prefix check. The new tests directly exercise the header-isolation invariant with two separate mockito servers. The two remaining findings (Debug leaking token, no base_url validation) are quality-of-life improvements that do not affect correctness in normal usage.

src/api.rs deserves a follow-up on the Debug impl and base_url validation; no other files require attention.

Important Files Changed

Filename Overview
src/api.rs Adds AttestationClientBuilder with configurable base_url; fixes github_headers to scope auth/version headers to the configured origin via a trailing-slash-appended prefix check — closes the prior spoofing gap — but the token is exposed via derived Debug and base_url() silently accepts malformed inputs.
src/lib.rs Re-exports AttestationClientBuilder alongside the existing public API surface; no logic changes.
tests/attestation_client_builder.rs Adds mockito integration tests covering default/custom base URL, trailing-slash normalisation, builder delegation, header injection for the configured host, and header suppression when downloading bundles from a different origin.

Sequence Diagram

sequenceDiagram
    participant Caller
    participant Builder as AttestationClientBuilder
    participant Client as AttestationClient
    participant GitHubAPI as GitHub API (base_url)
    participant BundleHost as Bundle Host (other origin)

    Caller->>Builder: base_url("https://proxy/github")
    Caller->>Builder: github_token("ghp_xxx")
    Caller->>Builder: build()
    Builder-->>Caller: AttestationClient { base_url, github_token }

    Caller->>Client: fetch_attestations(params)
    Client->>Client: github_headers(url)<br/>url starts with base_url+"/"
    Note right of Client: Authorization +<br/>x-github-api-version added
    Client->>GitHubAPI: GET /repos/.../attestations/{digest} + auth headers
    GitHubAPI-->>Client: {attestations: [{bundle_url: "https://other/bundle"}]}

    Client->>Client: github_headers(bundle_url)<br/>bundle_url ≠ base_url prefix
    Note right of Client: No auth headers added
    Client->>BundleHost: GET /bundle (no Authorization)
    BundleHost-->>Client: SigstoreBundle JSON
    Client-->>Caller: Vec<Attestation>
Loading

Fix All in Claude Code

Greploops — Automatically fix all review issues by running /greploops in Claude Code. It iterates: fix, push, re-review, repeat until 5/5 confidence.
Use the Greptile plugin for Claude Code to query reviews, search comments, and manage custom context directly from your terminal.

Reviews (2): Last reviewed commit: "fix: prevent token leakage via URL prefi..." | Re-trigger Greptile

Comment thread src/api.rs
Comment thread src/api.rs
Comment on lines +15 to +46
#[derive(Debug, Clone, Default)]
pub struct AttestationClientBuilder {
base_url: Option<String>,
github_token: Option<String>,
}

impl AttestationClientBuilder {
pub fn base_url(mut self, url: &str) -> Self {
self.base_url = Some(url.trim_end_matches('/').to_string());
self
}

pub fn github_token(mut self, token: &str) -> Self {
self.github_token = Some(token.to_string());
self
}

pub fn build(self) -> Result<AttestationClient> {
let mut headers = HeaderMap::new();
headers.insert(USER_AGENT, HeaderValue::from_static(USER_AGENT_VALUE));

let client = reqwest::Client::builder()
.default_headers(headers)
.build()?;

Ok(AttestationClient {
client,
base_url: self.base_url.unwrap_or_else(|| GITHUB_API_URL.to_string()),
github_token: self.github_token,
})
}
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 No tests for the new builder API

AttestationClientBuilder is new public API but the PR adds no tests for it. mockito is already a dev-dependency, so it would be straightforward to add tests covering:

  • That base_url() is used as the host for constructed request URLs
  • That auth headers are only sent to the configured host (directly exercises the security invariant in github_headers)
  • That AttestationClient::new() still produces a working client via its delegation to the builder

Fix in Claude Code

Use path-boundary check (trailing slash) in github_headers to prevent
auth tokens being sent to look-alike domains (e.g. api.github.com.evil.com).

Add tests covering the builder API, custom base URL routing, and the
security invariant that auth headers are only sent to the configured host.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@jdx
Copy link
Copy Markdown
Owner Author

jdx commented Apr 4, 2026

@greptileai check

@jdx jdx merged commit c40c811 into main Apr 4, 2026
8 checks passed
@jdx jdx deleted the feat/custom-github-api-url branch April 4, 2026 14:51
@jdx jdx mentioned this pull request Apr 3, 2026
jdx added a commit that referenced this pull request Apr 4, 2026
## 🤖 New release

* `sigstore-verification`: 0.2.1 -> 0.2.2 (✓ API compatible changes)

<details><summary><i><b>Changelog</b></i></summary><p>

<blockquote>

##
[0.2.2](v0.2.1...v0.2.2)
- 2026-04-04

### Added

- add builder pattern for customizable GitHub API URL
([#36](#36))

### Fixed

- generate Cargo.lock before security audit
([#24](#24))

### Other

- *(deps)* pin dtolnay/rust-toolchain action to 29eef33
([#33](#33))
- *(deps)* update jdx/mise-action digest to 1648a78
([#34](#34))
- *(deps)* update jdx/mise-action action to v4
([#31](#31))
- *(deps)* update swatinem/rust-cache digest to e18b497
([#30](#30))
- *(deps)* update release-plz/action digest to 1528104
([#29](#29))
- *(deps)* update jdx/mise-action digest to 5228313
([#28](#28))
- *(deps)* update jdx/mise-action digest to e79ddf6
([#27](#27))
- *(deps)* pin dependencies
([#26](#26))
</blockquote>


</p></details>

---
This PR was generated with
[release-plz](https://github.com/release-plz/release-plz/).

<!-- CURSOR_SUMMARY -->
---

> [!NOTE]
> **Low Risk**
> Low risk: this PR only updates release metadata (crate version and
changelog) without changing library code or behavior.
> 
> **Overview**
> Prepares the `v0.2.2` release by bumping `Cargo.toml` from `0.2.1` to
`0.2.2` and adding the corresponding `CHANGELOG.md` section.
> 
> The changelog notes the builder pattern for a customizable GitHub API
URL, a fix to generate `Cargo.lock` before security audit, and several
CI/dependency pin updates.
> 
> <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit
9b5a822. Bugbot is set up for automated
code reviews on this repo. Configure
[here](https://www.cursor.com/dashboard/bugbot).</sup>
<!-- /CURSOR_SUMMARY -->
@jdx jdx mentioned this pull request Apr 19, 2026
5 tasks
jdx added a commit that referenced this pull request Apr 19, 2026
## Summary

- Adds a `base_url` option to `GitHubSource` and
`verify_github_attestation`, so callers can verify attestations
published on GitHub Enterprise Server instances (e.g.
`https://github.enterprise.com/api/v3`) instead of always targeting
`api.github.com`.
- New surface (all additive, backward-compatible):
  - `GitHubSource::with_base_url(owner, repo, token, base_url)`
- `GitHubSource::with_client(owner, repo, client)` — hand in a pre-built
`AttestationClient`
  - `GitHubSource::builder()` fluent builder
- `verify_github_attestation_with_base_url(...)` convenience wrapper
mirroring `verify_github_attestation`
- Existing `GitHubSource::new` and `verify_github_attestation` keep
their current behavior (default to `api.github.com`).

## Motivation

mise users who point the github backend at a GHES instance via `api_url
= \"https://github.enterprise.com/api/v3\"` had their downloads succeed
against GHES, but attestation verification then fell back to
`api.github.com` and failed with a 401 because the GHES token isn't
valid there. Details: jdx/mise#9176

The underlying `AttestationClient::builder().base_url(...)` already
existed (#36); this PR plumbs it through the public `GitHubSource` and
`verify_github_attestation` surfaces that callers actually use.

## Test plan

- [x] `cargo test` — all existing tests still pass (10+6+3 in existing
suites).
- [x] New `tests/github_source_base_url.rs`:
`GitHubSource::with_base_url`, `::with_client`, and `::builder()` all
route requests to the configured host and send the auth token;
`builder()` surfaces missing-field errors.
- [x] New `tests/verify_github_attestation_base_url.rs`: end-to-end test
of `verify_github_attestation_with_base_url` against a mockito server,
asserting the request hits the custom host with the right auth header.
- [x] `cargo clippy --all-targets -- -D warnings` clean.
- [x] `cargo fmt --all --check` clean.

<!-- CURSOR_SUMMARY -->
---

> [!NOTE]
> **Medium Risk**
> Moderate risk: changes how GitHub API clients are constructed and
where auth tokens are sent, which can affect attestation
fetching/verification behavior across hosts (dotcom vs GHES). Additive
APIs and new tests reduce regression risk, but misconfiguration could
lead to unexpected unauthenticated requests.
> 
> **Overview**
> Enables attestation fetching and `verify_github_attestation` to target
a *custom GitHub API base URL* (e.g. GitHub Enterprise Server) instead
of always using `api.github.com`, by introducing
`verify_github_attestation_with_base_url` and refactoring verification
to build an `AttestationClient` with optional token + base URL.
> 
> Updates `GitHubSource` to support `with_base_url`, `with_client`, and
a fluent `builder()` (with required-field validation), and adds
mock-based tests to assert requests route to the configured host and
include the expected auth/version headers. The changelog is updated to
document the new capability.
> 
> <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit
dbe7653. Bugbot is set up for automated
code reviews on this repo. Configure
[here](https://www.cursor.com/dashboard/bugbot).</sup>
<!-- /CURSOR_SUMMARY -->
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.

Feature request: Allow customizing the GitHub API URL

1 participant