mirror of
https://github.com/safedep/pmg.git
synced 2026-08-03 07:24:09 +02:00
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:
@@ -126,6 +126,11 @@ func (t *bubblewrapPolicyTranslator) addIsolationNamespaces(policy *sandbox.Sand
|
||||
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
|
||||
if t.config.unsharePID {
|
||||
args = append(args, "--unshare-pid")
|
||||
|
||||
@@ -555,6 +555,22 @@ func (t *seatbeltPolicyTranslator) translateNetwork(policy *sandbox.SandboxPolic
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
|
||||
@@ -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) {
|
||||
translator := newSeatbeltPolicyTranslator()
|
||||
assert.NotEmpty(t, translator.LogTag())
|
||||
|
||||
@@ -34,6 +34,9 @@ type SandboxPolicy struct {
|
||||
|
||||
// AllowPTY allows pseudo-terminal (PTY) operations.
|
||||
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.
|
||||
@@ -50,6 +53,7 @@ type FilesystemPolicy struct {
|
||||
type NetworkPolicy struct {
|
||||
AllowOutbound []string `yaml:"allow_outbound" json:"allow_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.
|
||||
@@ -124,6 +128,7 @@ func (child *SandboxPolicy) MergeWithParent(parent *SandboxPolicy) {
|
||||
// Union network lists
|
||||
child.Network.AllowOutbound = unionStringSlices(parent.Network.AllowOutbound, child.Network.AllowOutbound)
|
||||
child.Network.DenyOutbound = unionStringSlices(parent.Network.DenyOutbound, child.Network.DenyOutbound)
|
||||
child.Network.AllowBind = unionStringSlices(parent.Network.AllowBind, child.Network.AllowBind)
|
||||
|
||||
// Union process lists
|
||||
child.Process.AllowExec = unionStringSlices(parent.Process.AllowExec, child.Process.AllowExec)
|
||||
@@ -137,6 +142,10 @@ func (child *SandboxPolicy) MergeWithParent(parent *SandboxPolicy) {
|
||||
if child.AllowGitConfig == nil {
|
||||
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.
|
||||
|
||||
@@ -13,6 +13,9 @@ package_managers:
|
||||
# in parent profiles in the future.
|
||||
allow_pty: true
|
||||
|
||||
# npx generators and dev servers frequently need to bind to localhost ports
|
||||
allow_network_bind: true
|
||||
|
||||
filesystem:
|
||||
# Add write permissions for common generator outputs
|
||||
allow_write:
|
||||
|
||||
Reference in New Issue
Block a user