fix: Sandbox Allow network bind for npx (#151)

* feat: Add support for network bind

* chore: Add comments for bwrap sandbox
This commit is contained in:
Abhisek Datta
2026-02-04 15:32:47 +05:30
committed by GitHub
parent f90fa0e6a5
commit 3778d4d1f3
5 changed files with 111 additions and 0 deletions
@@ -126,6 +126,11 @@ func (t *bubblewrapPolicyTranslator) addIsolationNamespaces(policy *sandbox.Sand
log.Debugf("Network allowed (no --unshare-net)") log.Debugf("Network allowed (no --unshare-net)")
} }
// Note: AllowNetworkBind and Network.AllowBind are not handled here because
// bwrap's --unshare-net creates a namespace with loopback available, so
// localhost binding already works. Non-localhost binding requires full host
// network (no --unshare-net), which is controlled by AllowOutbound rules.
// PID namespace isolation // PID namespace isolation
if t.config.unsharePID { if t.config.unsharePID {
args = append(args, "--unshare-pid") args = append(args, "--unshare-pid")
@@ -555,6 +555,22 @@ func (t *seatbeltPolicyTranslator) translateNetwork(policy *sandbox.SandboxPolic
sb.WriteString("\n") sb.WriteString("\n")
// Network bind rules for local listening
if utils.SafelyGetValue(policy.AllowNetworkBind) {
sb.WriteString(";; Local network bind (localhost only)\n")
sb.WriteString("(allow network-bind (local ip \"localhost:*\"))\n")
sb.WriteString("(allow network* (local ip \"localhost:*\"))\n")
}
for _, addr := range policy.Network.AllowBind {
sb.WriteString(fmt.Sprintf("(allow network-bind (local ip \"%s\"))\n", addr))
sb.WriteString(fmt.Sprintf("(allow network* (local ip \"%s\"))\n", addr))
}
if utils.SafelyGetValue(policy.AllowNetworkBind) || len(policy.Network.AllowBind) > 0 {
sb.WriteString("\n")
}
return nil return nil
} }
@@ -572,6 +572,84 @@ func TestPTYSupport(t *testing.T) {
}) })
} }
func TestNetworkBindSupport(t *testing.T) {
t.Run("AllowNetworkBind true with no AllowBind generates localhost-only bind rule", func(t *testing.T) {
policy := &sandbox.SandboxPolicy{
Name: "test",
Description: "test with network bind",
PackageManagers: []string{"npm"},
AllowNetworkBind: utils.PtrTo(true),
}
translator := newSeatbeltPolicyTranslator()
actual, err := translator.translate(policy)
assert.NoError(t, err)
assert.Contains(t, actual, ";; Local network bind (localhost only)")
assert.Contains(t, actual, `(allow network-bind (local ip "localhost:*"))`)
assert.Contains(t, actual, `(allow network* (local ip "localhost:*"))`)
// Should not contain any explicit bind addresses
assert.NotContains(t, actual, `(allow network-bind (local ip "0.0.0.0:*"))`)
})
t.Run("AllowNetworkBind true with AllowBind generates both rules", func(t *testing.T) {
policy := &sandbox.SandboxPolicy{
Name: "test",
Description: "test with network bind and explicit addresses",
PackageManagers: []string{"npm"},
AllowNetworkBind: utils.PtrTo(true),
Network: sandbox.NetworkPolicy{
AllowBind: []string{"0.0.0.0:*"},
},
}
translator := newSeatbeltPolicyTranslator()
actual, err := translator.translate(policy)
assert.NoError(t, err)
// Localhost bind from AllowNetworkBind
assert.Contains(t, actual, `(allow network-bind (local ip "localhost:*"))`)
// Explicit bind address
assert.Contains(t, actual, `(allow network-bind (local ip "0.0.0.0:*"))`)
})
t.Run("AllowNetworkBind false with AllowBind generates only explicit bind rule", func(t *testing.T) {
policy := &sandbox.SandboxPolicy{
Name: "test",
Description: "test with explicit bind only",
PackageManagers: []string{"npm"},
AllowNetworkBind: utils.PtrTo(false),
Network: sandbox.NetworkPolicy{
AllowBind: []string{"0.0.0.0:*"},
},
}
translator := newSeatbeltPolicyTranslator()
actual, err := translator.translate(policy)
assert.NoError(t, err)
// Should NOT contain localhost bind
assert.NotContains(t, actual, ";; Local network bind (localhost only)")
assert.NotContains(t, actual, `(allow network-bind (local ip "localhost:*"))`)
// Should contain explicit bind address
assert.Contains(t, actual, `(allow network-bind (local ip "0.0.0.0:*"))`)
})
t.Run("AllowNetworkBind nil with no AllowBind generates no bind rules", func(t *testing.T) {
policy := &sandbox.SandboxPolicy{
Name: "test",
Description: "test without network bind",
PackageManagers: []string{"npm"},
}
translator := newSeatbeltPolicyTranslator()
actual, err := translator.translate(policy)
assert.NoError(t, err)
assert.NotContains(t, actual, "network-bind")
})
}
func TestSeatbeltTranslatorDarwinLogTag(t *testing.T) { func TestSeatbeltTranslatorDarwinLogTag(t *testing.T) {
translator := newSeatbeltPolicyTranslator() translator := newSeatbeltPolicyTranslator()
assert.NotEmpty(t, translator.LogTag()) assert.NotEmpty(t, translator.LogTag())
+9
View File
@@ -34,6 +34,9 @@ type SandboxPolicy struct {
// AllowPTY allows pseudo-terminal (PTY) operations. // AllowPTY allows pseudo-terminal (PTY) operations.
AllowPTY *bool `yaml:"allow_pty" json:"allow_pty"` AllowPTY *bool `yaml:"allow_pty" json:"allow_pty"`
// AllowNetworkBind allows binding to localhost (127.0.0.1 / ::1) for listening.
AllowNetworkBind *bool `yaml:"allow_network_bind" json:"allow_network_bind"`
} }
// FilesystemPolicy defines allowed and denied filesystem access patterns. // FilesystemPolicy defines allowed and denied filesystem access patterns.
@@ -50,6 +53,7 @@ type FilesystemPolicy struct {
type NetworkPolicy struct { type NetworkPolicy struct {
AllowOutbound []string `yaml:"allow_outbound" json:"allow_outbound"` AllowOutbound []string `yaml:"allow_outbound" json:"allow_outbound"`
DenyOutbound []string `yaml:"deny_outbound" json:"deny_outbound"` DenyOutbound []string `yaml:"deny_outbound" json:"deny_outbound"`
AllowBind []string `yaml:"allow_bind" json:"allow_bind"`
} }
// ProcessPolicy defines allowed and denied process execution patterns. // ProcessPolicy defines allowed and denied process execution patterns.
@@ -124,6 +128,7 @@ func (child *SandboxPolicy) MergeWithParent(parent *SandboxPolicy) {
// Union network lists // Union network lists
child.Network.AllowOutbound = unionStringSlices(parent.Network.AllowOutbound, child.Network.AllowOutbound) child.Network.AllowOutbound = unionStringSlices(parent.Network.AllowOutbound, child.Network.AllowOutbound)
child.Network.DenyOutbound = unionStringSlices(parent.Network.DenyOutbound, child.Network.DenyOutbound) child.Network.DenyOutbound = unionStringSlices(parent.Network.DenyOutbound, child.Network.DenyOutbound)
child.Network.AllowBind = unionStringSlices(parent.Network.AllowBind, child.Network.AllowBind)
// Union process lists // Union process lists
child.Process.AllowExec = unionStringSlices(parent.Process.AllowExec, child.Process.AllowExec) child.Process.AllowExec = unionStringSlices(parent.Process.AllowExec, child.Process.AllowExec)
@@ -137,6 +142,10 @@ func (child *SandboxPolicy) MergeWithParent(parent *SandboxPolicy) {
if child.AllowGitConfig == nil { if child.AllowGitConfig == nil {
child.AllowGitConfig = utils.PtrTo(utils.SafelyGetValue(parent.AllowGitConfig)) child.AllowGitConfig = utils.PtrTo(utils.SafelyGetValue(parent.AllowGitConfig))
} }
if child.AllowNetworkBind == nil {
child.AllowNetworkBind = utils.PtrTo(utils.SafelyGetValue(parent.AllowNetworkBind))
}
} }
// unionStringSlices returns a new slice containing all unique elements from both slices. // unionStringSlices returns a new slice containing all unique elements from both slices.
+3
View File
@@ -13,6 +13,9 @@ package_managers:
# in parent profiles in the future. # in parent profiles in the future.
allow_pty: true allow_pty: true
# npx generators and dev servers frequently need to bind to localhost ports
allow_network_bind: true
filesystem: filesystem:
# Add write permissions for common generator outputs # Add write permissions for common generator outputs
allow_write: allow_write: