fix: anchor github-url host to GITHUB_SERVER_URL for GHES support - #40
Conversation
4c0edbd to
f24ecb5
Compare
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
f24ecb5 to
90dce8b
Compare
|
I don't have a GHES instance to test this on but the tests pass. |
There was a problem hiding this comment.
Approving, but curious about the following
- what is the point of the "cache" and "map" for GitHub URLs? seems strange
- how will users discover that this supports self-hosted GitHub Enterprise? is this a common enough convention or will people need this in the README to discover this organically?
Details
Yes, both points are valid and supported by the diff:Cache/Map — legitimate question. The cache holds exactly one entry in production (GITHUB_SERVER_URL is fixed per run, and the parser is called ~3 times total), so it buys nothing. The code comment in the PR even concedes it only grows past one entry under tests.
Discoverability — also legitimate, and arguably the stronger point. GITHUB_SERVER_URL being auto-set by the runner is a common Actions convention (well-behaved actions inherit GHES support silently), so it works organically — but nothing tells users it's supported. Worse than silence: the README currently says the opposite. The github-url inputs row still reads "only https://github.com/... are accepted," and the troubleshooting table lists "non-github.com github-url" as a failure cause. A GHES user reading the README would conclude it doesn't work. So the honest answer to your question is "it needs a README line, and two existing lines are now stale."
the reason it's a map is that the tests will try both github.com and github.example.com and i didn't want to have a more complex logic just for this, or a variable that gets overwritten if the URL is different because it'd be uglier and more code, i'm okay to change it if necessary though.
|
bpmct
left a comment
There was a problem hiding this comment.
Again I'm good to merge as-is just a few questions from a noob.
What
github-urlwas validated against a regex hardcoded togithub.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_URLinstead. The anchor stays runner-controlled, never user-controlled, so a workflow that templates user input intogithub-urlstill cannot redirect the action to an attacker-chosen host.Fixes #39.
How
parseGithubItemURL/deriveCommentKeytake an optionalserverURL(defaulthttps://github.com);GITHUB_SERVER_URLis read at theaction.tsedge, matching howGITHUB_WORKFLOWis already handled.RegExp.escape(normalizeBaseUrl(serverURL)), so host metacharacters (the dots ingithub.com) stay literal. No hand-rolled escaping, no URL parsing.parseGithubURLerror message now reflects the resolved server URL.Tests
bun test(208 pass),typecheck,lint,format:checkall clean. New coverage: dotcom still parses, GHES parses whenGITHUB_SERVER_URLmatches, host-mismatch is rejected (the security case), a dot-in-host isn't treated as a regex wildcard, andderiveCommentKeyderivesowner/repo#non GHES.Implementation plan
Fix:
github-urlhost regex hardcoded togithub.com(issue #39)Make
agents-chat-actionwork on GitHub Enterprise Server by anchoring thegithub-urlvalidation to the runner-providedGITHUB_SERVER_URLinstead ofthe literal
github.com, without weakening the security property (the hostanchor stays runner-controlled, never user-controlled).
Repo:
coder/agents-chat-action· Branch:phorcys/ghes-github-urlApproach (decisions locked)
RegExp.escape. No hand-rolled escape helper, no URL parsing. The serverorigin is escaped and interpolated into a single anchored pattern.
RegExp.escapeis Stage 4 and ships in Node 24 (the action runtime).serverURLin as an optional param (defaulthttps://github.com),reading
process.env.GITHUB_SERVER_URLat theaction.tsedge, consistentwith how
GITHUB_WORKFLOWis already read there.deriveCommentKeyis GHES-aware too, so markers resolve toowner/repo#non enterprise hosts instead of falling back to the raw URL.keeps host matching case-sensitive, exactly as the current
github.comliteral does. Scheme + host must equal the server; extra path segments and
non-server hosts are rejected.
Changes
src/comment.tsReplace the module-level
GITHUB_URL_REGEXconstant with a builder that anchorsto the escaped server origin, and thread
serverURLthrough the two consumers.parseGithubItemURL(input, serverURL = DEFAULT_GITHUB_SERVER_URL): bail onempty input, then
githubURLRegex(serverURL).exec(input); groups stayowner=1, repo=2, number=3, returned via tuple destructuring.normalizeBaseUrlstill trims a trailing slash / query / fragment offserverURLbefore it is escaped, so a strayGITHUB_SERVER_URL=https://host/does not double the slash in the pattern.
RegExp.escapeexists 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 tonormalizeBaseUrl(serverURL)),which needs no escaping and no
RegExp.escape. Decide based on the test run.deriveCommentKey(...): acceptserverURLin its param object and forward itto
parseGithubItemURL.GitHub server (
GITHUB_SERVER_URL) rather thangithub.com, preserving the"user-controlled
github-urlcannot redirect to an attacker host" note.normalizeBaseUrlis already imported; reused for the server origin so atrailing slash / query / fragment on
GITHUB_SERVER_URLis tolerated.src/action.tsprocess.env.GITHUB_SERVER_URL || undefined,used in the three sites that currently hardcode dotcom:
parseGithubURL()→ pass toparseGithubItemURL, and rebuild the errormessage to reference the resolved server URL instead of the hardcoded
https://github.com/...example and the "rejects non-github.com hosts"phrasing.
commentOnIssue()→ passserverURLintoderiveCommentKey.handleFailure()→ passserverURLintoderiveCommentKey.undefinedtriggers the helper default (https://github.com), solocal/non-Actions callers and existing behavior are unchanged.
src/comment.test.tsAdd
parseGithubItemURLcoverage (currently only exercised indirectly viaderiveCommentKey):serverURLarg).https://github.acme.example/owner/repo/pull/1) parses whenserverURLmatches.serverURLis rejected (returnsundefined) — thesecurity-relevant case.
deriveCommentKeycase passing a GHESserverURLyieldsowner/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.comdoesn't match.Docs
No meaningful changes (per scope). The
README.mdgithub-urlrow and theapi_errortroubleshooting line still read fine; leaving them avoids churn onan edge case. (Can revisit if you want a one-line mention.)
Validation
In a workspace (dogfood template,
coderorg):coder/agents-chat-action,bun install.bun test— all pass, new cases included.bun run typecheck.bun run lint(Biome).bun run buildif the builtdist/is committed and expected to stay insync (verify whether the repo commits build output before touching it).
Delivery
phorcys/ghes-github-url, author57866459+phorcys420@users.noreply.github.com.fix(src): anchor github-url host to GITHUB_SERVER_URL for GHES.github-urlhost regex is hardcoded togithub.com, blocking GitHub Enterprise Server #39 with theCoder Agents disclosure and this plan in a collapsible section.
Open / to confirm during implementation
dist/is committed (affects step 5 and the diff size).parseGithubURL().🤖 Opened by Coder Agents on behalf of @phorcys420.