Commit 11114e655e

11114e655e6c7525cf76f9154b950ab9de3eb66f

parent: 1d81484849

Verified · cmc

cmc <hello@cleberg.net> · 2026-09-29 02:50 UTC

control: import-issues refuses credentials, a query or a fragment in --api-base

Ref #301

Layout: unified · split

internal/control/ghimport.go +4
@@ -173,6 +173,10 @@ func runImportIssues(c *Ctx, args []string) int {
173173 given := apiBase != ""
174174 if !given {
175175 apiBase = "https://api.github.com"
176 } else if strings.Contains(apiBase, "@") || strings.ContainsAny(apiBase, "?#") {
177 // Same rule as repo import: the URL is echoed and becomes the
178 // site prefix, so credentials and queries stay out of it.
179 return c.fail(protocol.ExitUsage, "--api-base: use the plain API URL, without credentials, a query or a fragment; a token goes on stdin with --token-stdin")
176180 }
177181 // A writer-supplied API base is the same SSRF surface as a webhook
178182 // target. Resolve and check it once here; the client connects only
internal/control/ghimport_test.go +13
@@ -176,3 +176,16 @@ func TestImportIssuesRefusesAPrivateAPIBase(t *testing.T) {
176176 t.Fatalf("looked up %v", *asked)
177177 }
178178}
179
180// --api-base takes a plain http or https URL: no credentials, query,
181// fragment or other scheme, and nothing of a refused one is echoed.
182func TestImportIssuesRefusesAnAPIBaseShape(t *testing.T) {
183 for _, base := range []string{"https://abc@api.test", "https://api.test/?abc", "https://api.test/#abc", "ftp://api.test/abc"} {
184 c, errOut, _, _ := importCtx(t, true)
185 asked := stubLookup(t, "127.0.0.1")
186 code := Dispatch(c, []string{"repo", "import-issues", "alice/app", "--from", "o/r", "--api-base", base})
187 if code != protocol.ExitUsage || len(*asked) != 0 || strings.Contains(errOut.String(), "abc") {
188 t.Fatalf("%s: exit %d, looked up %v: %s", base, code, *asked, errOut.String())
189 }
190 }
191}