Skip to content

fix: anchor github-url host to GITHUB_SERVER_URL for GHES support - #40

Draft
phorcys420 wants to merge 1 commit into
mainfrom
phorcys/ghes-github-url
Draft

fix: anchor github-url host to GITHUB_SERVER_URL for GHES support#40
phorcys420 wants to merge 1 commit into
mainfrom
phorcys/ghes-github-url

Conversation

@phorcys420

@phorcys420 phorcys420 commented Aug 24, 2026

Copy link
Copy Markdown
Member

What

github-url was validated against a regex hardcoded to github.com, so on GitHub Enterprise Server every issue/PR URL (https://github.example.com/owner/repo/pull/1) failed validation and the action exited before commenting.

This anchors the host to the runner-provided GITHUB_SERVER_URL instead. The anchor stays runner-controlled, never user-controlled, so a workflow that templates user input into github-url still cannot redirect the action to an attacker-chosen host.

Fixes #39.

How

  • parseGithubItemURL / deriveCommentKey take an optional serverURL (default https://github.com); GITHUB_SERVER_URL is read at the action.ts edge, matching how GITHUB_WORKFLOW is already handled.
  • The matcher is built with the built-in RegExp.escape(normalizeBaseUrl(serverURL)), so host metacharacters (the dots in github.com) stay literal. No hand-rolled escaping, no URL parsing.
  • Host anchoring semantics are unchanged (scheme + host must equal the server; extra path segments and non-server hosts rejected).
  • The parseGithubURL error message now reflects the resolved server URL.

Tests

bun test (208 pass), typecheck, lint, format:check all clean. New coverage: dotcom still parses, GHES parses when GITHUB_SERVER_URL matches, host-mismatch is rejected (the security case), a dot-in-host isn't treated as a regex wildcard, and deriveCommentKey derives owner/repo#n on GHES.

Implementation plan

Fix: github-url host regex hardcoded to github.com (issue #39)

Make agents-chat-action work on GitHub Enterprise Server by anchoring the
github-url validation to the runner-provided GITHUB_SERVER_URL instead of
the literal github.com, without weakening the security property (the host
anchor stays runner-controlled, never user-controlled).

Repo: coder/agents-chat-action · Branch: phorcys/ghes-github-url

Approach (decisions locked)

  • Build one regex from the server URL, escaped with the built-in
    RegExp.escape.
    No hand-rolled escape helper, no URL parsing. The server
    origin is escaped and interpolated into a single anchored pattern.
    RegExp.escape is Stage 4 and ships in Node 24 (the action runtime).
  • Plumb serverURL in as an optional param (default https://github.com),
    reading process.env.GITHUB_SERVER_URL at the action.ts edge, consistent
    with how GITHUB_WORKFLOW is already read there.
  • deriveCommentKey is GHES-aware too, so markers resolve to
    owner/repo#n on enterprise hosts instead of falling back to the raw URL.
  • Host anchoring semantics preserved and unchanged: the escaped literal
    keeps host matching case-sensitive, exactly as the current github.com
    literal does. Scheme + host must equal the server; extra path segments and
    non-server hosts are rejected.
  • Error messages reflect the real server URL; no extensive docs rewrite.

Changes

src/comment.ts

Replace the module-level GITHUB_URL_REGEX constant with a builder that anchors
to the escaped server origin, and thread serverURL through the two consumers.

const DEFAULT_GITHUB_SERVER_URL = "https://github.com";

// Anchored issue/PR URL matcher for a given GitHub server. RegExp.escape
// neutralizes metacharacters in the host (e.g. the dots in github.com) so it
// matches literally. Anchored at both ends; extra path segments are rejected.
// The trailing (?:[?#].*)? keeps query strings and fragments tolerated.
function githubURLRegex(serverURL: string): RegExp {
	const base = RegExp.escape(normalizeBaseUrl(serverURL));
	return new RegExp(
		`^${base}/([^/]+)/([^/]+)/(?:issues|pull)/(\\d+)/?(?:[?#].*)?$`,
	);
}
  • parseGithubItemURL(input, serverURL = DEFAULT_GITHUB_SERVER_URL): bail on
    empty input, then githubURLRegex(serverURL).exec(input); groups stay
    owner=1, repo=2, number=3, returned via tuple destructuring.
  • normalizeBaseUrl still trims a trailing slash / query / fragment off
    serverURL before it is escaped, so a stray GITHUB_SERVER_URL=https://host/
    does not double the slash in the pattern.
  • Runtime check (must verify in the workspace): confirm RegExp.escape
    exists in the pinned Bun test runtime. It is guaranteed on Node 24 (action
    runtime) but Bun's JSC support is version-dependent. If the pinned Bun lacks
    it, fall back to the generic-host capture-and-compare variant (static regex
    with (https:\/\/[^/]+) origin group compared to normalizeBaseUrl(serverURL)),
    which needs no escaping and no RegExp.escape. Decide based on the test run.
  • deriveCommentKey(...): accept serverURL in its param object and forward it
    to parseGithubItemURL.
  • Update the doc comments on both to say the host is anchored to the current
    GitHub server (GITHUB_SERVER_URL) rather than github.com, preserving the
    "user-controlled github-url cannot redirect to an attacker host" note.
  • normalizeBaseUrl is already imported; reused for the server origin so a
    trailing slash / query / fragment on GITHUB_SERVER_URL is tolerated.

src/action.ts

  • Add a small edge read, e.g. process.env.GITHUB_SERVER_URL || undefined,
    used in the three sites that currently hardcode dotcom:
    • parseGithubURL() → pass to parseGithubItemURL, and rebuild the error
      message to reference the resolved server URL instead of the hardcoded
      https://github.com/... example and the "rejects non-github.com hosts"
      phrasing.
    • commentOnIssue() → pass serverURL into deriveCommentKey.
    • handleFailure() → pass serverURL into deriveCommentKey.
  • Passing undefined triggers the helper default (https://github.com), so
    local/non-Actions callers and existing behavior are unchanged.

src/comment.test.ts

Add parseGithubItemURL coverage (currently only exercised indirectly via
deriveCommentKey):

  • dotcom URL parses with the default server (no serverURL arg).
  • GHES URL (e.g. https://github.acme.example/owner/repo/pull/1) parses when
    serverURL matches.
  • host that does not match serverURL is rejected (returns undefined) — the
    security-relevant case.
  • a deriveCommentKey case passing a GHES serverURL yields owner/repo#n
    (not the raw-URL fallback).

The existing deriveCommentKey "falls back to raw URL for non-github.com host"
test still passes: with the default server, code.acme.com doesn't match.

Docs

No meaningful changes (per scope). The README.md github-url row and the
api_error troubleshooting line still read fine; leaving them avoids churn on
an edge case. (Can revisit if you want a one-line mention.)

Validation

In a workspace (dogfood template, coder org):

  1. Clone coder/agents-chat-action, bun install.
  2. bun test — all pass, new cases included.
  3. bun run typecheck.
  4. bun run lint (Biome).
  5. bun run build if the built dist/ is committed and expected to stay in
    sync (verify whether the repo commits build output before touching it).

Delivery

Open / to confirm during implementation

  • Whether dist/ is committed (affects step 5 and the diff size).
  • Exact final error-message wording in parseGithubURL().

🤖 Opened by Coder Agents on behalf of @phorcys420.

@phorcys420
phorcys420 force-pushed the phorcys/ghes-github-url branch 4 times, most recently from 4c0edbd to f24ecb5 Compare August 24, 2026 22:40
The github-url host was hardcoded to github.com, so every issue/PR URL
on GitHub Enterprise Server failed validation and the action exited
before commenting.

Anchor the host to the runner-provided GITHUB_SERVER_URL instead. The
anchor stays runner-controlled, not user-controlled, so a workflow that
templates user input into github-url still cannot redirect the action to
an attacker-chosen host.

- parseGithubItemURL/deriveCommentKey take an optional serverURL
  (default https://github.com); env is read at the action.ts edge.
- Build the matcher with RegExp.escape so host metacharacters stay
  literal, no hand-rolled escaping.
- Error message reflects the resolved server URL.

Fixes #39
@phorcys420
phorcys420 force-pushed the phorcys/ghes-github-url branch from f24ecb5 to 90dce8b Compare August 24, 2026 22:44
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.

github-url host regex is hardcoded to github.com, blocking GitHub Enterprise Server

1 participant