Skip to content

Commit 8b23683

Browse files
authored
Validate that all allowed network IPs are specified (#2150)
1 parent 6404b58 commit 8b23683

3 files changed

Lines changed: 67 additions & 2 deletions

File tree

packages/api/internal/handlers/sandbox_create.go

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -513,7 +513,7 @@ func validateNetworkConfig(network *api.SandboxNetworkConfig) *api.APIError {
513513
// - when allowOut contains domains, denyOut must include 0.0.0.0/0
514514
func validateEgressRules(allowOut, denyOut []string) *api.APIError {
515515
for _, cidr := range denyOut {
516-
if !sandbox_network.IsIPOrCIDR(cidr) {
516+
if !sandbox_network.IsSpecifiedIPOrCIDR(cidr) {
517517
return &api.APIError{
518518
Code: http.StatusBadRequest,
519519
Err: fmt.Errorf("invalid denied CIDR %s", cidr),
@@ -523,7 +523,17 @@ func validateEgressRules(allowOut, denyOut []string) *api.APIError {
523523
}
524524

525525
if len(allowOut) > 0 {
526-
_, allowedDomains := sandbox_network.ParseAddressesAndDomains(allowOut)
526+
allowedAddresses, allowedDomains := sandbox_network.ParseAddressesAndDomains(allowOut)
527+
528+
for _, addr := range allowedAddresses {
529+
if !sandbox_network.IsSpecifiedIPOrCIDR(addr) {
530+
return &api.APIError{
531+
Code: http.StatusBadRequest,
532+
Err: fmt.Errorf("invalid allowed address %s", addr),
533+
ClientMsg: fmt.Sprintf("invalid allowed address %s", addr),
534+
}
535+
}
536+
}
527537
hasBlockAll := slices.Contains(denyOut, sandbox_network.AllInternetTrafficCIDR)
528538

529539
if len(allowedDomains) > 0 && !hasBlockAll {

packages/shared/pkg/sandbox-network/firewall.go

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,32 @@ func IsIPOrCIDR(s string) bool {
6565
return err == nil
6666
}
6767

68+
// IsSpecifiedIPOrCIDR checks if a string is a valid IP address or CIDR notation
69+
// with a specified (non-zero) IP. It rejects unspecified addresses like 0.0.0.0
70+
// or :: (which cause errors in nftables), but allows 0.0.0.0/0 as a special case.
71+
func IsSpecifiedIPOrCIDR(s string) bool {
72+
if !IsIPOrCIDR(s) {
73+
return false
74+
}
75+
76+
// Allow the special all-traffic CIDR
77+
if s == AllInternetTrafficCIDR {
78+
return true
79+
}
80+
81+
// Extract the IP portion
82+
if ip := net.ParseIP(s); ip != nil {
83+
return !ip.IsUnspecified()
84+
}
85+
86+
ip, _, err := net.ParseCIDR(s)
87+
if err != nil {
88+
return false
89+
}
90+
91+
return !ip.IsUnspecified()
92+
}
93+
6894
// ParseAddressesAndDomains separates a list of strings into IP addresses/CIDRs and domain names.
6995
func ParseAddressesAndDomains(entries []string) (addresses []string, domains []string) {
7096
for _, entry := range entries {

packages/shared/pkg/sandbox-network/firewall_test.go

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,35 @@ import (
66
"github.com/stretchr/testify/require"
77
)
88

9+
func TestIsSpecifiedIPOrCIDR(t *testing.T) {
10+
t.Parallel()
11+
testCases := []struct {
12+
name string
13+
input string
14+
expected bool
15+
}{
16+
{"valid_ip", "1.2.3.4", true},
17+
{"valid_cidr", "10.0.0.0/8", true},
18+
{"valid_host_cidr", "192.168.1.1/32", true},
19+
{"all_traffic_cidr", "0.0.0.0/0", true},
20+
{"unspecified_ip", "0.0.0.0", false},
21+
{"unspecified_cidr_32", "0.0.0.0/32", false},
22+
{"unspecified_cidr_24", "0.0.0.0/24", false},
23+
{"unspecified_ipv6", "::", false},
24+
{"unspecified_ipv6_128", "::/128", false},
25+
{"invalid_string", "not-an-ip", false},
26+
{"empty_string", "", false},
27+
}
28+
29+
for _, tc := range testCases {
30+
t.Run(tc.name, func(t *testing.T) {
31+
t.Parallel()
32+
result := IsSpecifiedIPOrCIDR(tc.input)
33+
require.Equal(t, tc.expected, result)
34+
})
35+
}
36+
}
37+
938
func TestAddressStringToCIDR(t *testing.T) {
1039
t.Parallel()
1140
testCases := []struct {

0 commit comments

Comments
 (0)