Commit 592e7bcb92

592e7bcb92db6735547556da02eadca4d6634cdf

parent: ecfc702093

Verified · cmc ci/build: success ci/test: success

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

import: document the address check; e2e imports with allow_local

Closes #298

Layout: unified · split

.gitbay/wiki/Admin.org +4 −2
@@ -193,12 +193,14 @@ push=.
193193 means no credential-bearing HTTP endpoint exists at all.
194194
195195** [webhooks]
196- =allow_local= (false) — permit webhook and mirror targets on
196- =allow_local= (false) — permit webhook, mirror and import targets on
197197 loopback, private, shared (100.64.0.0/10), link-local or multicast
198198 addresses. Leave off unless you know why you need it (SSRF).
199199
200200** [limits]
201- =clone_timeout= (3600s) — cap on =repo import= fetches.
201- =clone_timeout= (3600s) — cap on =repo import= fetches. An import
202 takes http and https URLs only, passes the same address check as a
203 mirror sync and is pinned the same way, so it needs git 2.37 too.
202204- =max_blob_bytes= (100MB) — cap on raw file serving over the web.
203205- =max_asset_bytes= (512MB) — cap per uploaded release asset.
204206- =max_snippet_bytes= (1MB) — cap per snippet file.
.gitbay/wiki/Architecture/02-Components.org +1
@@ -32,6 +32,7 @@ the hidden =hook= used by git (=cmd/gitbayd/main.go=,
3232| =internal/lfs= | Content-addressed LFS store and HMAC transfer tokens. |
3333| =internal/webhook= | Outbound webhook delivery with SSRF checks, HMAC signing, retries. |
3434| =internal/mirror= | Push and pull mirror worker. |
35| =internal/gitpin= | Resolves and checks a user-supplied http(s) remote and pins git to the checked addresses; mirror sync and =repo import=. |
3536| =internal/notify=, =internal/mail= | Mail queue drain and SMTP. |
3637| =internal/push= | APNs queue drain and provider-token signing. |
3738| =internal/deps= | Dependency manifest parsing and registry checks (opt-in per repository). |
.gitbay/wiki/Architecture/09-Controls.org +1 −1
@@ -75,7 +75,7 @@ chapter names of OWASP ASVS 4.0 where one fits.
7575
7676| Control | Status | Evidence |
7777|---------------------------------------------+----------+------------------------------------------------------------------|
78| SSRF protection on user-supplied URLs | partial | webhooks at save and connect; mirrors at save and sync, git pinned to the checked address (=internal/mirror/mirror.go=); =repo import --from= has no address check (#298) |
78| SSRF protection on user-supplied URLs | in place | webhooks at save and connect; mirrors at save and sync, =repo import= before its fetch, git pinned to the checked address (=internal/gitpin=) |
7979| Webhook payload integrity | in place | HMAC-SHA256 header |
8080| SMTP credentials protected in transit | in place | STARTTLS required for non-local relays, implicit TLS optional (=internal/mail/mail.go=) |
8181| Upload size limits | in place | per-owner storage quota at push (=internal/sshd/sshd.go=); API body 1 MiB |
.gitbay/wiki/Architecture/10-Known-Gaps.org +1 −1
@@ -14,7 +14,7 @@ what the 2026-09-27 review found; remove a row when its issue closes.
1414| #260 | CI network | Builds share the runner's source address; no egress policy | medium |
1515| #261 | Various | Migration foreign-key check after commit; three web writes bypass dispatch; documentation drift | medium |
1616| #297 | Credentials | A browser session can mint tokens and keys that outlive it | low |
17| #298 | SSRF | =repo import --from= fetches without an address check | medium |
17| #301 | SSRF | =repo import-issues --api-base= fetches without an address check or pin | medium |
1818
1919* Not filed
2020
.gitbay/wiki/Threat-Model.org +14 −11
@@ -91,18 +91,21 @@ no inbound HMAC.
9191
9292* Network-facing request forgery
9393
94Webhook delivery, GitHub-history import =--api-base= and mirror
95remotes, which make the *server* open an outbound connection to a
96user-supplied address, pass the same SSRF guard: the scheme
97must be http/https and, unless =webhooks.allow_local= is set, the
98resolved address must not be loopback, private, shared
94Webhook delivery, GitHub-history import =--api-base=, mirror remotes
95and =repo import --from=, which make the *server* open an outbound
96connection to a user-supplied address, pass the same SSRF guard: the
97scheme must be http/https and, unless =webhooks.allow_local= is set,
98the resolved address must not be loopback, private, shared
9999(100.64.0.0/10), link-local, or multicast. The webhook dialer re-checks
100at connect time, and the mirror worker resolves and checks before each
101sync and pins git to the checked addresses, so a DNS answer that
102changes after validation still cannot reach private space. Redirects
103are never followed. =repo import --from= is the exception: its clone
104checks the scheme but not the address, and follows git's default
105redirect rule (#298).
100at connect time, and mirror sync and =repo import= resolve and check
101immediately before running git and pin it to the checked addresses
102(=internal/gitpin=), so a DNS answer that changes after validation
103still cannot reach private space. Redirects are never followed.
104=repo import= refuses =git://=, which cannot be pinned, and a host
105written as a bare number or in hex/octal (=0x7f.1=, =2130706433=,
106=127.1=) rather than dotted decimal, since that form resolves
107differently across parsers. GitHub-history import (=repo
108import-issues --api-base=) is not yet pinned (#301).
106109
107110* Rendering pushed markup
108111
.gitbay/wiki/Users.org +4
@@ -256,6 +256,10 @@ gitbay repo import-issues you/mirror --from you/repo \
256256 --api-base https://codeberg.org/api/v1 # Forgejo: the site's /api/v1
257257#+end_src
258258
259=repo import= fetches over http and https only, from an address that
260passes the same check as a webhook target; a =git://= URL is refused,
261so use the repository's https URL.
262
259263Sourcehut has no API of that shape; =repo import= takes its git data
260264and the todo.sr.ht tracker is not read.
261265
CHANGELOG.org +12
@@ -11,6 +11,18 @@ anything beyond "replace the binary and restart" is needed.
1111 listings and shell history. A script that passed the value must pipe
1212 it: =printf %s "$SECRET" | gitbay webhook add <repo> <url> --secret -=
1313 (#284).
14- =repo import --from= takes http and https URLs only; =git://= is
15 refused, since its connection cannot be held to a checked address.
16 The host is resolved and checked like a mirror's, git connects only
17 to the checked addresses with redirects off, and the system and
18 global gitconfig are ignored. A source that redirects (a renamed
19 repository) fails; import from the URL it redirects to. Needs git
20 2.37 or later on the server (#298).
21*Upgrade note.* =repo import= and mirror sync now refuse a =git://=
22source and a =--from=/remote URL carrying a query or fragment. A
23mirror or import whose host is written numerically (=127.1=,
24=2130706433=, =0x7f.1=) rather than as a dotted address is refused
25too; rewrite it before upgrading.
1426- The builds page's status badge section gives an org-mode snippet
1527 beside the Markdown one, for a README.org (#299).
1628- API tokens on the settings page: create with a scope and optional
e2e/import_test.go +4 −12
@@ -11,7 +11,7 @@ import (
1111
1212func TestRepoImport(t *testing.T) {
1313 t.Parallel()
14 inst := startInstance(t)
14 inst := startInstanceWith(t, "[webhooks]\nallow_local = true\n")
1515 aliceKey := inst.newKey(t, "alice")
1616 inst.admin(t, "admin", "user", "create", "alice",
1717 "--key", aliceKey+".pub", "--email", "alice@example.test", "--verified")
@@ -82,15 +82,6 @@ func TestRepoImport(t *testing.T) {
8282 t.Fatalf("hooks not wired on imported repo:\n%s", pushOut)
8383 }
8484
85 // Import over git:// too.
86 if _, errOut, code = inst.ssh(t, aliceKey, "", "repo", "settings", "git-daemon", "alice/src", "on"); code != 0 {
87 t.Fatalf("git-daemon on: %s", errOut)
88 }
89 gitURL := fmt.Sprintf("git://127.0.0.1:%d/alice/src.git", inst.gitPort)
90 if _, errOut, code = inst.ssh(t, aliceKey, "", "repo", "import", "alice/mirror2", "--from", gitURL); code != 0 {
91 t.Fatalf("git:// import: %s", errOut)
92 }
93
9485 // Org-owned imports: allowed for org admins, refused for non-members.
9586 if _, errOut, code := inst.ssh(t, aliceKey, "", "org", "create", "imports"); code != 0 {
9687 t.Fatalf("org create: %s", errOut)
@@ -102,12 +93,13 @@ func TestRepoImport(t *testing.T) {
10293 t.Fatalf("org import log: %d\n%s", code, out)
10394 }
10495
105 // Refusals: bad scheme, credentials in URL, existing name, foreign owner.
96 // Refusals: bad scheme, git://, credentials in URL, existing name, foreign owner.
10697 cases := []struct {
10798 args []string
10899 want string
109100 }{
110 {[]string{"repo", "import", "alice/x", "--from", "file:///etc"}, "https://, http://, and git://"},
101 {[]string{"repo", "import", "alice/x", "--from", "file:///etc"}, "http:// and https:// only"},
102 {[]string{"repo", "import", "alice/x", "--from", "git://127.0.0.1:1/alice/src.git"}, "use the repository's https:// URL"},
111103 {[]string{"repo", "import", "alice/x", "--from", "https://token@github.com/a/b"}, "--token-stdin"},
112104 {[]string{"repo", "import", "alice/mirror", "--from", httpURL}, "already exists"},
113105 {[]string{"repo", "import", "bob/x", "--from", httpURL}, "not you and not an organization"},