Commit 592e7bcb92
Verified · cmc ci/build: success ci/test: success
Layout: unified · split
.gitbay/wiki/Admin.org +4 −2
| @@ -193,12 +193,14 @@ push=. | ||
| 193 | 193 | means no credential-bearing HTTP endpoint exists at all. |
| 194 | 194 | |
| 195 | 195 | ** [webhooks] |
| 196 | - =allow_local= (false) — permit webhook and mirror targets on | |
| 196 | - =allow_local= (false) — permit webhook, mirror and import targets on | |
| 197 | 197 | loopback, private, shared (100.64.0.0/10), link-local or multicast |
| 198 | 198 | addresses. Leave off unless you know why you need it (SSRF). |
| 199 | 199 | |
| 200 | 200 | ** [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. | |
| 202 | 204 | - =max_blob_bytes= (100MB) — cap on raw file serving over the web. |
| 203 | 205 | - =max_asset_bytes= (512MB) — cap per uploaded release asset. |
| 204 | 206 | - =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=, | ||
| 32 | 32 | | =internal/lfs= | Content-addressed LFS store and HMAC transfer tokens. | |
| 33 | 33 | | =internal/webhook= | Outbound webhook delivery with SSRF checks, HMAC signing, retries. | |
| 34 | 34 | | =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=. | | |
| 35 | 36 | | =internal/notify=, =internal/mail= | Mail queue drain and SMTP. | |
| 36 | 37 | | =internal/push= | APNs queue drain and provider-token signing. | |
| 37 | 38 | | =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. | ||
| 75 | 75 | |
| 76 | 76 | | Control | Status | Evidence | |
| 77 | 77 | |---------------------------------------------+----------+------------------------------------------------------------------| |
| 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=) | | |
| 79 | 79 | | Webhook payload integrity | in place | HMAC-SHA256 header | |
| 80 | 80 | | SMTP credentials protected in transit | in place | STARTTLS required for non-local relays, implicit TLS optional (=internal/mail/mail.go=) | |
| 81 | 81 | | 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. | ||
| 14 | 14 | | #260 | CI network | Builds share the runner's source address; no egress policy | medium | |
| 15 | 15 | | #261 | Various | Migration foreign-key check after commit; three web writes bypass dispatch; documentation drift | medium | |
| 16 | 16 | | #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 | | |
| 18 | 18 | |
| 19 | 19 | * Not filed |
| 20 | 20 | |
.gitbay/wiki/Threat-Model.org +14 −11
| @@ -91,18 +91,21 @@ no inbound HMAC. | ||
| 91 | 91 | |
| 92 | 92 | * Network-facing request forgery |
| 93 | 93 | |
| 94 | Webhook delivery, GitHub-history import =--api-base= and mirror | |
| 95 | remotes, which make the *server* open an outbound connection to a | |
| 96 | user-supplied address, pass the same SSRF guard: the scheme | |
| 97 | must be http/https and, unless =webhooks.allow_local= is set, the | |
| 98 | resolved address must not be loopback, private, shared | |
| 94 | Webhook delivery, GitHub-history import =--api-base=, mirror remotes | |
| 95 | and =repo import --from=, which make the *server* open an outbound | |
| 96 | connection to a user-supplied address, pass the same SSRF guard: the | |
| 97 | scheme must be http/https and, unless =webhooks.allow_local= is set, | |
| 98 | the resolved address must not be loopback, private, shared | |
| 99 | 99 | (100.64.0.0/10), link-local, or multicast. The webhook dialer re-checks |
| 100 | at connect time, and the mirror worker resolves and checks before each | |
| 101 | sync and pins git to the checked addresses, so a DNS answer that | |
| 102 | changes after validation still cannot reach private space. Redirects | |
| 103 | are never followed. =repo import --from= is the exception: its clone | |
| 104 | checks the scheme but not the address, and follows git's default | |
| 105 | redirect rule (#298). | |
| 100 | at connect time, and mirror sync and =repo import= resolve and check | |
| 101 | immediately before running git and pin it to the checked addresses | |
| 102 | (=internal/gitpin=), so a DNS answer that changes after validation | |
| 103 | still cannot reach private space. Redirects are never followed. | |
| 104 | =repo import= refuses =git://=, which cannot be pinned, and a host | |
| 105 | written as a bare number or in hex/octal (=0x7f.1=, =2130706433=, | |
| 106 | =127.1=) rather than dotted decimal, since that form resolves | |
| 107 | differently across parsers. GitHub-history import (=repo | |
| 108 | import-issues --api-base=) is not yet pinned (#301). | |
| 106 | 109 | |
| 107 | 110 | * Rendering pushed markup |
| 108 | 111 | |
.gitbay/wiki/Users.org +4
| @@ -256,6 +256,10 @@ gitbay repo import-issues you/mirror --from you/repo \ | ||
| 256 | 256 | --api-base https://codeberg.org/api/v1 # Forgejo: the site's /api/v1 |
| 257 | 257 | #+end_src |
| 258 | 258 | |
| 259 | =repo import= fetches over http and https only, from an address that | |
| 260 | passes the same check as a webhook target; a =git://= URL is refused, | |
| 261 | so use the repository's https URL. | |
| 262 | ||
| 259 | 263 | Sourcehut has no API of that shape; =repo import= takes its git data |
| 260 | 264 | and the todo.sr.ht tracker is not read. |
| 261 | 265 | |
CHANGELOG.org +12
| @@ -11,6 +11,18 @@ anything beyond "replace the binary and restart" is needed. | ||
| 11 | 11 | listings and shell history. A script that passed the value must pipe |
| 12 | 12 | it: =printf %s "$SECRET" | gitbay webhook add <repo> <url> --secret -= |
| 13 | 13 | (#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://= | |
| 22 | source and a =--from=/remote URL carrying a query or fragment. A | |
| 23 | mirror or import whose host is written numerically (=127.1=, | |
| 24 | =2130706433=, =0x7f.1=) rather than as a dotted address is refused | |
| 25 | too; rewrite it before upgrading. | |
| 14 | 26 | - The builds page's status badge section gives an org-mode snippet |
| 15 | 27 | beside the Markdown one, for a README.org (#299). |
| 16 | 28 | - API tokens on the settings page: create with a scope and optional |
e2e/import_test.go +4 −12
| @@ -11,7 +11,7 @@ import ( | ||
| 11 | 11 | |
| 12 | 12 | func TestRepoImport(t *testing.T) { |
| 13 | 13 | t.Parallel() |
| 14 | inst := startInstance(t) | |
| 14 | inst := startInstanceWith(t, "[webhooks]\nallow_local = true\n") | |
| 15 | 15 | aliceKey := inst.newKey(t, "alice") |
| 16 | 16 | inst.admin(t, "admin", "user", "create", "alice", |
| 17 | 17 | "--key", aliceKey+".pub", "--email", "alice@example.test", "--verified") |
| @@ -82,15 +82,6 @@ func TestRepoImport(t *testing.T) { | ||
| 82 | 82 | t.Fatalf("hooks not wired on imported repo:\n%s", pushOut) |
| 83 | 83 | } |
| 84 | 84 | |
| 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 | ||
| 94 | 85 | // Org-owned imports: allowed for org admins, refused for non-members. |
| 95 | 86 | if _, errOut, code := inst.ssh(t, aliceKey, "", "org", "create", "imports"); code != 0 { |
| 96 | 87 | t.Fatalf("org create: %s", errOut) |
| @@ -102,12 +93,13 @@ func TestRepoImport(t *testing.T) { | ||
| 102 | 93 | t.Fatalf("org import log: %d\n%s", code, out) |
| 103 | 94 | } |
| 104 | 95 | |
| 105 | // Refusals: bad scheme, credentials in URL, existing name, foreign owner. | |
| 96 | // Refusals: bad scheme, git://, credentials in URL, existing name, foreign owner. | |
| 106 | 97 | cases := []struct { |
| 107 | 98 | args []string |
| 108 | 99 | want string |
| 109 | 100 | }{ |
| 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"}, | |
| 111 | 103 | {[]string{"repo", "import", "alice/x", "--from", "https://token@github.com/a/b"}, "--token-stdin"}, |
| 112 | 104 | {[]string{"repo", "import", "alice/mirror", "--from", httpURL}, "already exists"}, |
| 113 | 105 | {[]string{"repo", "import", "bob/x", "--from", httpURL}, "not you and not an organization"}, |