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 | means no credential-bearing HTTP endpoint exists at all. | 193 | means no credential-bearing HTTP endpoint exists at all. |
| 194 | 194 | ||
| 195 | ** [webhooks] | 195 | ** [webhooks] |
| 196 | - =allow_local= (false) — permit webhook and mirror targets on | 196 | - =allow_local= (false) — permit webhook, mirror and import targets on |
| 197 | loopback, private, shared (100.64.0.0/10), link-local or multicast | 197 | loopback, private, shared (100.64.0.0/10), link-local or multicast |
| 198 | addresses. Leave off unless you know why you need it (SSRF). | 198 | addresses. Leave off unless you know why you need it (SSRF). |
| 199 | 199 | ||
| 200 | ** [limits] | 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 | - =max_blob_bytes= (100MB) — cap on raw file serving over the web. | 204 | - =max_blob_bytes= (100MB) — cap on raw file serving over the web. |
| 203 | - =max_asset_bytes= (512MB) — cap per uploaded release asset. | 205 | - =max_asset_bytes= (512MB) — cap per uploaded release asset. |
| 204 | - =max_snippet_bytes= (1MB) — cap per snippet file. | 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 | | =internal/lfs= | Content-addressed LFS store and HMAC transfer tokens. | | 32 | | =internal/lfs= | Content-addressed LFS store and HMAC transfer tokens. | |
| 33 | | =internal/webhook= | Outbound webhook delivery with SSRF checks, HMAC signing, retries. | | 33 | | =internal/webhook= | Outbound webhook delivery with SSRF checks, HMAC signing, retries. | |
| 34 | | =internal/mirror= | Push and pull mirror worker. | | 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 | | =internal/notify=, =internal/mail= | Mail queue drain and SMTP. | | 36 | | =internal/notify=, =internal/mail= | Mail queue drain and SMTP. | |
| 36 | | =internal/push= | APNs queue drain and provider-token signing. | | 37 | | =internal/push= | APNs queue drain and provider-token signing. | |
| 37 | | =internal/deps= | Dependency manifest parsing and registry checks (opt-in per repository). | | 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 | | Control | Status | Evidence | | 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 | | Webhook payload integrity | in place | HMAC-SHA256 header | | 79 | | Webhook payload integrity | in place | HMAC-SHA256 header | |
| 80 | | SMTP credentials protected in transit | in place | STARTTLS required for non-local relays, implicit TLS optional (=internal/mail/mail.go=) | | 80 | | SMTP credentials protected in transit | in place | STARTTLS required for non-local relays, implicit TLS optional (=internal/mail/mail.go=) | |
| 81 | | Upload size limits | in place | per-owner storage quota at push (=internal/sshd/sshd.go=); API body 1 MiB | | 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 | | #260 | CI network | Builds share the runner's source address; no egress policy | medium | | 14 | | #260 | CI network | Builds share the runner's source address; no egress policy | medium | |
| 15 | | #261 | Various | Migration foreign-key check after commit; three web writes bypass dispatch; documentation drift | medium | | 15 | | #261 | Various | Migration foreign-key check after commit; three web writes bypass dispatch; documentation drift | medium | |
| 16 | | #297 | Credentials | A browser session can mint tokens and keys that outlive it | low | | 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 | * Not filed | 19 | * Not filed |
| 20 | 20 | ||
.gitbay/wiki/Threat-Model.org +14 −11
| @@ -91,18 +91,21 @@ no inbound HMAC. | |||
| 91 | 91 | ||
| 92 | * Network-facing request forgery | 92 | * Network-facing request forgery |
| 93 | 93 | ||
| 94 | Webhook delivery, GitHub-history import =--api-base= and mirror | 94 | Webhook delivery, GitHub-history import =--api-base=, mirror remotes |
| 95 | remotes, which make the *server* open an outbound connection to a | 95 | and =repo import --from=, which make the *server* open an outbound |
| 96 | user-supplied address, pass the same SSRF guard: the scheme | 96 | connection to a user-supplied address, pass the same SSRF guard: the |
| 97 | must be http/https and, unless =webhooks.allow_local= is set, the | 97 | scheme must be http/https and, unless =webhooks.allow_local= is set, |
| 98 | resolved address must not be loopback, private, shared | 98 | the resolved address must not be loopback, private, shared |
| 99 | (100.64.0.0/10), link-local, or multicast. The webhook dialer re-checks | 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 | 100 | at connect time, and mirror sync and =repo import= resolve and check |
| 101 | sync and pins git to the checked addresses, so a DNS answer that | 101 | immediately before running git and pin it to the checked addresses |
| 102 | changes after validation still cannot reach private space. Redirects | 102 | (=internal/gitpin=), so a DNS answer that changes after validation |
| 103 | are never followed. =repo import --from= is the exception: its clone | 103 | still cannot reach private space. Redirects are never followed. |
| 104 | checks the scheme but not the address, and follows git's default | 104 | =repo import= refuses =git://=, which cannot be pinned, and a host |
| 105 | redirect rule (#298). | 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 | * Rendering pushed markup | 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 | --api-base https://codeberg.org/api/v1 # Forgejo: the site's /api/v1 | 256 | --api-base https://codeberg.org/api/v1 # Forgejo: the site's /api/v1 |
| 257 | #+end_src | 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 | Sourcehut has no API of that shape; =repo import= takes its git data | 263 | Sourcehut has no API of that shape; =repo import= takes its git data |
| 260 | and the todo.sr.ht tracker is not read. | 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 | listings and shell history. A script that passed the value must pipe | 11 | listings and shell history. A script that passed the value must pipe |
| 12 | it: =printf %s "$SECRET" | gitbay webhook add <repo> <url> --secret -= | 12 | it: =printf %s "$SECRET" | gitbay webhook add <repo> <url> --secret -= |
| 13 | (#284). | 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 | - The builds page's status badge section gives an org-mode snippet | 26 | - The builds page's status badge section gives an org-mode snippet |
| 15 | beside the Markdown one, for a README.org (#299). | 27 | beside the Markdown one, for a README.org (#299). |
| 16 | - API tokens on the settings page: create with a scope and optional | 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 | func TestRepoImport(t *testing.T) { | 12 | func TestRepoImport(t *testing.T) { |
| 13 | t.Parallel() | 13 | t.Parallel() |
| 14 | inst := startInstance(t) | 14 | inst := startInstanceWith(t, "[webhooks]\nallow_local = true\n") |
| 15 | aliceKey := inst.newKey(t, "alice") | 15 | aliceKey := inst.newKey(t, "alice") |
| 16 | inst.admin(t, "admin", "user", "create", "alice", | 16 | inst.admin(t, "admin", "user", "create", "alice", |
| 17 | "--key", aliceKey+".pub", "--email", "alice@example.test", "--verified") | 17 | "--key", aliceKey+".pub", "--email", "alice@example.test", "--verified") |
| @@ -82,15 +82,6 @@ func TestRepoImport(t *testing.T) { | |||
| 82 | t.Fatalf("hooks not wired on imported repo:\n%s", pushOut) | 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 | // Org-owned imports: allowed for org admins, refused for non-members. | 85 | // Org-owned imports: allowed for org admins, refused for non-members. |
| 95 | if _, errOut, code := inst.ssh(t, aliceKey, "", "org", "create", "imports"); code != 0 { | 86 | if _, errOut, code := inst.ssh(t, aliceKey, "", "org", "create", "imports"); code != 0 { |
| 96 | t.Fatalf("org create: %s", errOut) | 87 | t.Fatalf("org create: %s", errOut) |
| @@ -102,12 +93,13 @@ func TestRepoImport(t *testing.T) { | |||
| 102 | t.Fatalf("org import log: %d\n%s", code, out) | 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 | cases := []struct { | 97 | cases := []struct { |
| 107 | args []string | 98 | args []string |
| 108 | want string | 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 | {[]string{"repo", "import", "alice/x", "--from", "https://token@github.com/a/b"}, "--token-stdin"}, | 103 | {[]string{"repo", "import", "alice/x", "--from", "https://token@github.com/a/b"}, "--token-stdin"}, |
| 112 | {[]string{"repo", "import", "alice/mirror", "--from", httpURL}, "already exists"}, | 104 | {[]string{"repo", "import", "alice/mirror", "--from", httpURL}, "already exists"}, |
| 113 | {[]string{"repo", "import", "bob/x", "--from", httpURL}, "not you and not an organization"}, | 105 | {[]string{"repo", "import", "bob/x", "--from", httpURL}, "not you and not an organization"}, |