Skip to content

Commit e37d34e

Browse files
committed
fix: match forge hostnames exactly or by subdomain
Forge hostnames are matched exactly or by subdomain instead of by substring. So hostile hostnames like "github.com.evil.example" or "mygithub.company.com" are no longer matched, and no user's token can leak.
1 parent ffe9d50 commit e37d34e

3 files changed

Lines changed: 22 additions & 9 deletions

File tree

‎DOCUMENTATION.md‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ It runs on both Firefox and Chrome from a single source tree:
3636
Runs on every tab update:
3737

3838
1. **Parse the URL**: extract `projectKey`, `repoSlug` and `filepath`
39-
2. **Detect the forge** by hostname
39+
2. **Detect the forge** by hostname (exact match or subdomain)
4040
3. **Load the auth headers** for the forge from the stored token (if any)
4141
4. **Promise chain**: list PRs, get files, keep PRs including the file
4242
5. **Render**: set the toolbar badge text and stores `{ tabId, prs }` in
@@ -134,7 +134,7 @@ Here is the interface for each forge:
134134
| Member | Purpose |
135135
| ------------ | ------------------------------------------------------------------ |
136136
| `label` | human-readable forge name |
137-
| `hostnames` | array of hostnames matched against the page (one per family instance) |
137+
| `hostnames` | array of hostnames matched against the page, exactly or as a subdomain (one per family instance) |
138138
| `base_url` | API root URL the endpoint builders are hung off of |
139139
| `parseUrl` | `(URL) → { projectKey, repoSlug, filepath, origin }` or `null` |
140140
| `listPRsUrl` | API endpoint listing the repo's open PRs |

‎forges.js‎

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,9 @@
88
//
99
// Adapter interface:
1010
// label human-readable forge name, shown in the options page UI
11-
// hostnames array of substrings matched against the page hostname (for a
12-
// family instance this is its single configured hostname)
11+
// hostnames array of hostnames matched against the page hostname, exactly
12+
// or as a subdomain (for a family instance this is its single
13+
// configured hostname)
1314
// base_url API root URL the endpoint builders below are hung off of
1415
// parseUrl (URL) -> { projectKey, repoSlug, filepath, origin } | null
1516
// listPRsUrl (info) -> API endpoint listing the repo's open pull requests
@@ -394,10 +395,14 @@ export function buildSelfHostedForge({ type, hostname }) {
394395

395396
// pick a forge adapter for a page hostname, considering both the built-in
396397
// static forges and the user's configured self-hosted instances (null if none
397-
// matches). Static forges win; self-hosted instances are matched by exact
398-
// hostname to avoid one instance shadowing another.
398+
// matches). Static forges win; they match the exact hostname or a subdomain of
399+
// it — never a mere substring, so a hostile lookalike host (github.com.evil.example)
400+
// or an unrelated self-hosted forge (mygithub.company.com) stays unmatched.
401+
// Self-hosted instances are matched by exact hostname to avoid one instance
402+
// shadowing another.
399403
export function forgeForHostname(hostname, instances = []) {
400-
const staticForge = staticForges.find(f => f.hostnames.some(h => hostname.includes(h)))
404+
const staticForge = staticForges.find(f =>
405+
f.hostnames.some(h => hostname === h || hostname.endsWith(`.${h}`)))
401406
if (staticForge) return staticForge
402407

403408
const instance = instances.find(i => i.hostname === hostname)

‎tests/forges.test.js‎

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,15 +26,23 @@ test("forgeForHostname matches gitlab.com", () => {
2626
assert.equal(forgeForHostname("gitlab.com"), gitlab)
2727
})
2828

29-
test("forgeForHostname matches a subdomain containing the hostname", () => {
30-
// hostnames are matched as substrings of the page hostname
29+
test("forgeForHostname matches a subdomain of a forge hostname", () => {
3130
assert.equal(forgeForHostname("www.github.com"), github)
3231
})
3332

3433
test("forgeForHostname returns null for an unknown forge", () => {
3534
assert.equal(forgeForHostname("example.com"), null)
3635
})
3736

37+
test("forgeForHostname does not match a forge hostname as a mere substring", () => {
38+
// a hostile lookalike host must not be treated as the real forge (it would
39+
// trigger API calls carrying the user's token for attacker-chosen paths)
40+
assert.equal(forgeForHostname("github.com.evil.example"), null)
41+
// an unrelated host merely containing the name is not the public forge
42+
assert.equal(forgeForHostname("mygithub.company.com"), null)
43+
assert.equal(forgeForHostname("evil-gitlab.com"), null)
44+
})
45+
3846
//
3947
// github.parseUrl
4048
//

0 commit comments

Comments
 (0)