From ec47691609f91b2739487d4e99602f47957f783c Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Sat, 10 Oct 2026 20:42:56 -0500 Subject: [PATCH] fix(leaf): grant unauthenticated peers capabilities explicitly; go-libfossil v0.11.0 go-libfossil v0.11.0 gives a new repository's `nobody` user fossil's default capabilities, clone and pull (`gjorz`), instead of push and Setup (GHSA-44p2-whvx-9gwq). Leaves that accept pushes from peers syncing without credentials, over HTTP, NATS or iroh, now have to grant that explicitly. - agent.Config.NobodyCaps, applied by agent.New, sets the leaf repo's nobody capabilities. Empty keeps what the repo stores. - The leaf binary takes it as -nobody-caps / LEAF_NOBODY_CAPS. For example, `gio` lets unauthenticated peers push to this leaf. - The leaf-to-leaf iroh tests grant `gio`, since their peers push without credentials. The bump also brings v0.11.0's other security fixes: the login nonce is checked against the request body, reqconfig is scoped, payload sizes and table-sync rows are validated, and HTTP replies are capped. --- bridge/go.mod | 2 +- bridge/go.sum | 4 ++-- go.mod | 2 +- go.sum | 4 ++-- leaf/agent/agent.go | 7 +++++++ leaf/agent/config.go | 9 +++++++++ leaf/cmd/leaf/main.go | 2 ++ leaf/go.mod | 2 +- leaf/go.sum | 4 ++-- sim/iroh_test.go | 4 ++++ sim/nats_iroh_test.go | 1 + 11 files changed, 32 insertions(+), 9 deletions(-) diff --git a/bridge/go.mod b/bridge/go.mod index 0b7c52c..1af8b7f 100644 --- a/bridge/go.mod +++ b/bridge/go.mod @@ -4,7 +4,7 @@ go 1.26.0 require ( github.com/danmestas/EdgeSync/leaf v0.0.3 - github.com/danmestas/go-libfossil v0.10.0 + github.com/danmestas/go-libfossil v0.11.0 github.com/danmestas/go-libfossil/db/driver/modernc v0.3.0 github.com/nats-io/nats-server/v2 v2.14.5 github.com/nats-io/nats.go v1.53.1 diff --git a/bridge/go.sum b/bridge/go.sum index c59ccc5..932a4da 100644 --- a/bridge/go.sum +++ b/bridge/go.sum @@ -2,8 +2,8 @@ github.com/antithesishq/antithesis-sdk-go v0.7.2 h1:oEEedg1Xgi8drRjqB0f9tfjhLoIn github.com/antithesishq/antithesis-sdk-go v0.7.2/go.mod h1:FQyySiasQQM8735Ddel3MRojmy4dA1IqCeyJ5jmPMbI= github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= -github.com/danmestas/go-libfossil v0.10.0 h1:POA4uzYgIv3iJhTMwLM1wGmGo5FnzOBtyd1BwR6H/lo= -github.com/danmestas/go-libfossil v0.10.0/go.mod h1:srcBH/pQu2vKaebmyTIQcwJ5Jev2PUtL76JQXxEy7mQ= +github.com/danmestas/go-libfossil v0.11.0 h1:oqd/1f0CooP7940Ten8i2NF5yRtZFjEvybbXqEn+otQ= +github.com/danmestas/go-libfossil v0.11.0/go.mod h1:srcBH/pQu2vKaebmyTIQcwJ5Jev2PUtL76JQXxEy7mQ= github.com/danmestas/go-libfossil/db/driver/modernc v0.3.0 h1:O01NTY1LOkD9rcjneDAti5SC4RGvSARrrWtfnXyehBs= github.com/danmestas/go-libfossil/db/driver/modernc v0.3.0/go.mod h1:spdUGmH76DTTxNqmE2BaH3PvzGkilaNYnGQbaHO3T7M= github.com/danmestas/go-libfossil/db/driver/ncruces v0.3.0 h1:/0g6Ye9NFh6M9eH2n8shS/ahlyKK9Xtwl9w8K5/MEbE= diff --git a/go.mod b/go.mod index 0b2afab..e55b96c 100644 --- a/go.mod +++ b/go.mod @@ -6,7 +6,7 @@ require ( github.com/alecthomas/kong v1.16.1 github.com/danmestas/EdgeSync/bridge v0.0.4 github.com/danmestas/EdgeSync/leaf v0.0.12 - github.com/danmestas/go-libfossil v0.10.0 + github.com/danmestas/go-libfossil v0.11.0 github.com/danmestas/go-libfossil/db/driver/modernc v0.3.0 github.com/nats-io/nats-server/v2 v2.14.5 github.com/nats-io/nats.go v1.53.1 diff --git a/go.sum b/go.sum index 76587e1..8455280 100644 --- a/go.sum +++ b/go.sum @@ -8,8 +8,8 @@ github.com/antithesishq/antithesis-sdk-go v0.7.2 h1:oEEedg1Xgi8drRjqB0f9tfjhLoIn github.com/antithesishq/antithesis-sdk-go v0.7.2/go.mod h1:FQyySiasQQM8735Ddel3MRojmy4dA1IqCeyJ5jmPMbI= github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= -github.com/danmestas/go-libfossil v0.10.0 h1:POA4uzYgIv3iJhTMwLM1wGmGo5FnzOBtyd1BwR6H/lo= -github.com/danmestas/go-libfossil v0.10.0/go.mod h1:srcBH/pQu2vKaebmyTIQcwJ5Jev2PUtL76JQXxEy7mQ= +github.com/danmestas/go-libfossil v0.11.0 h1:oqd/1f0CooP7940Ten8i2NF5yRtZFjEvybbXqEn+otQ= +github.com/danmestas/go-libfossil v0.11.0/go.mod h1:srcBH/pQu2vKaebmyTIQcwJ5Jev2PUtL76JQXxEy7mQ= github.com/danmestas/go-libfossil/db/driver/modernc v0.3.0 h1:O01NTY1LOkD9rcjneDAti5SC4RGvSARrrWtfnXyehBs= github.com/danmestas/go-libfossil/db/driver/modernc v0.3.0/go.mod h1:spdUGmH76DTTxNqmE2BaH3PvzGkilaNYnGQbaHO3T7M= github.com/danmestas/go-libfossil/db/driver/ncruces v0.3.0 h1:/0g6Ye9NFh6M9eH2n8shS/ahlyKK9Xtwl9w8K5/MEbE= diff --git a/leaf/agent/agent.go b/leaf/agent/agent.go index f64f031..c982850 100644 --- a/leaf/agent/agent.go +++ b/leaf/agent/agent.go @@ -101,6 +101,13 @@ func New(cfg Config) (*Agent, error) { } applySQLiteTuning(r) + if cfg.NobodyCaps != "" { + if err := r.SetCaps("nobody", cfg.NobodyCaps); err != nil { + r.Close() + return nil, fmt.Errorf("agent: apply NobodyCaps: %w", err) + } + } + projectCode, err := r.Config("project-code") if err != nil { r.Close() diff --git a/leaf/agent/config.go b/leaf/agent/config.go index addf070..6e6c026 100644 --- a/leaf/agent/config.go +++ b/leaf/agent/config.go @@ -106,6 +106,15 @@ type Config struct { // project sync subject. Enables leaf-to-leaf sync without a bridge. ServeNATSEnabled bool + // NobodyCaps, when non-empty, replaces the capabilities of the repo's + // "nobody" user at startup. Peers that sync with this leaf without + // credentials (HTTP, NATS or iroh serving) get exactly these. Empty + // keeps whatever the repo stores; a repo libfossil creates or clones + // grants nobody clone and pull ("gjorz") but not push. Set "gio" to + // let unauthenticated peers push to this leaf. Applied by New; + // NewFromParts does no I/O and ignores it. + NobodyCaps string + // CustomDialer overrides the default net.Dial for NATS connections. // Set to &wsdialer.WSDialer{URL: "ws://..."} for browser WebSocket. CustomDialer nats.CustomDialer diff --git a/leaf/cmd/leaf/main.go b/leaf/cmd/leaf/main.go index 0018f57..815742e 100644 --- a/leaf/cmd/leaf/main.go +++ b/leaf/cmd/leaf/main.go @@ -32,6 +32,7 @@ func main() { pull := flag.Bool("pull", true, "enable pull") serveHTTP := flag.String("serve-http", envOrDefault("LEAF_SERVE_HTTP", ""), "HTTP listen address (e.g. :8080) to serve fossil clone/sync") serveNATS := flag.Bool("serve-nats", false, "enable NATS request/reply listener for leaf-to-leaf sync") + nobodyCaps := flag.String("nobody-caps", envOrDefault("LEAF_NOBODY_CAPS", ""), "capabilities for peers syncing without credentials (e.g. gio to accept their pushes); empty keeps the repo's") uv := flag.Bool("uv", false, "enable unversioned file sync (wiki, forum, attachments)") iroh := flag.Bool("iroh", envBool("LEAF_IROH"), "enable iroh sidecar for peer-to-peer sync") irohKeyPath := flag.String("iroh-key", envOrDefault("LEAF_IROH_KEY", ""), "path to iroh Ed25519 keypair (default: .iroh-key)") @@ -103,6 +104,7 @@ func main() { UV: *uv, ServeHTTPAddr: *serveHTTP, ServeNATSEnabled: *serveNATS, + NobodyCaps: *nobodyCaps, Observer: obs, IrohEnabled: *iroh, IrohPeers: irohPeers, diff --git a/leaf/go.mod b/leaf/go.mod index 66a8166..604470a 100644 --- a/leaf/go.mod +++ b/leaf/go.mod @@ -3,7 +3,7 @@ module github.com/danmestas/EdgeSync/leaf go 1.26.0 require ( - github.com/danmestas/go-libfossil v0.10.0 + github.com/danmestas/go-libfossil v0.11.0 github.com/danmestas/go-libfossil/db/driver/modernc v0.3.0 github.com/danmestas/go-libfossil/db/driver/ncruces v0.3.0 github.com/nats-io/nats-server/v2 v2.14.5 diff --git a/leaf/go.sum b/leaf/go.sum index 474548e..f1f3f66 100644 --- a/leaf/go.sum +++ b/leaf/go.sum @@ -10,8 +10,8 @@ github.com/chromedp/chromedp v0.15.0 h1:B5abPcaCVetu6GDHsQKj8HbMhDWdiOM52E/TuOfu github.com/chromedp/chromedp v0.15.0/go.mod h1:V5szO2ASqoBZsIQ88EfNMRhi9737Equm5HFL9tsBt3Q= github.com/chromedp/sysutil v1.1.0 h1:PUFNv5EcprjqXZD9nJb9b/c9ibAbxiYo4exNWZyipwM= github.com/chromedp/sysutil v1.1.0/go.mod h1:WiThHUdltqCNKGc4gaU50XgYjwjYIhKWoHGPTUfWTJ8= -github.com/danmestas/go-libfossil v0.10.0 h1:POA4uzYgIv3iJhTMwLM1wGmGo5FnzOBtyd1BwR6H/lo= -github.com/danmestas/go-libfossil v0.10.0/go.mod h1:srcBH/pQu2vKaebmyTIQcwJ5Jev2PUtL76JQXxEy7mQ= +github.com/danmestas/go-libfossil v0.11.0 h1:oqd/1f0CooP7940Ten8i2NF5yRtZFjEvybbXqEn+otQ= +github.com/danmestas/go-libfossil v0.11.0/go.mod h1:srcBH/pQu2vKaebmyTIQcwJ5Jev2PUtL76JQXxEy7mQ= github.com/danmestas/go-libfossil/db/driver/modernc v0.3.0 h1:O01NTY1LOkD9rcjneDAti5SC4RGvSARrrWtfnXyehBs= github.com/danmestas/go-libfossil/db/driver/modernc v0.3.0/go.mod h1:spdUGmH76DTTxNqmE2BaH3PvzGkilaNYnGQbaHO3T7M= github.com/danmestas/go-libfossil/db/driver/ncruces v0.3.0 h1:/0g6Ye9NFh6M9eH2n8shS/ahlyKK9Xtwl9w8K5/MEbE= diff --git a/sim/iroh_test.go b/sim/iroh_test.go index 0b8d7b1..0d94099 100644 --- a/sim/iroh_test.go +++ b/sim/iroh_test.go @@ -137,6 +137,10 @@ func TestIrohConvergence(t *testing.T) { if err != nil { t.Fatalf("open leaf-%d: %v", i, err) } + // leaf-0 pushes to leaf-1 without credentials. + if err := r.SetCaps("nobody", "gio"); err != nil { + t.Fatalf("leaf-%d: grant nobody push: %v", i, err) + } a := agent.NewFromParts(agent.Config{ RepoPath: leafPaths[i], Push: true, diff --git a/sim/nats_iroh_test.go b/sim/nats_iroh_test.go index ecc5986..ac1fa20 100644 --- a/sim/nats_iroh_test.go +++ b/sim/nats_iroh_test.go @@ -83,6 +83,7 @@ func TestNATSOverIrohSync(t *testing.T) { IrohKeyPath: filepath.Join(dir, fmt.Sprintf("leaf-%d.iroh-key", i)), IrohBinaryPath: sidecarBin, ServeNATSEnabled: true, + NobodyCaps: "gio", // peers push to each other without credentials PeerID: fmt.Sprintf("test-peer-%d", i), Logger: func(msg string) { t.Logf("[leaf-%d] %s", idx, msg)