Skip to content

Commit d2a5931

Browse files
committed
Merged PR 13618357: guest/network: Restrict hostname to valid characters
[cherry-picked from 055ee5eb4a802cb407575fb6cc1e9b07069d3319] guest/network: Restrict hostname to valid characters Because we write this hostname to /etc/hosts, without proper validation the host can trick us into writing arbitrary data to /etc/hosts, which can, for example, redirect things like ip6-localhost (but likely not localhost itself) to an attacker-controlled IP address. We implement a check here that the host-provided DNS name in the OCI spec is valid. ACI actually restricts this to 5-63 characters of a-zA-Z0-9 and '-', where the first and last characters can not be '-'. This aligns with the Kubernetes restriction. c.f. IsValidDnsLabel in Compute-ACI. However, there is no consistent official agreement on what a valid hostname can contain. RFC 952 says that "Domain name" can be up to 24 characters of A-Z0-9 '.' and '-', RFC 1123 expands this to 255 characters, but RFC 1035 claims that domain names can in fact contain anything if quoted (as long as the length is within 255 characters), and this is confirmed again in RFC 2181. In practice we see names with underscopes, most commonly \_dmarc.example.com. curl allows 0-9a-zA-Z and -.\_|~ and any other codepoints from \u0001-\u001f and above \u007f: https://github.com/curl/curl/blob/master/lib/urlapi.c#L527-L545 With the above in mind, this commit allows up to 255 characters of a-zA-Z0-9 and '_', '-' and '.'. This change is applied to all LCOW use cases. This fix can be tested with the below code to bypass any host-side checks: +++ b/internal/hcsoci/hcsdoc_lcow.go @@ -52,6 +52,10 @@ func createLCOWSpec(ctx context.Context, coi *createOptionsInternal) (*specs.Spe spec.Linux.Seccomp = nil } + if spec.Annotations[annotations.KubernetesContainerType] == "sandbox" { + spec.Hostname = "invalid-hostname\n1.1.1.1 ip6-localhost ip6-loopback localhost" + } + return spec, nil } Output: time="2025-10-01T15:13:41Z" level=fatal msg="run pod sandbox: rpc error: code = Unknown desc = failed to create containerd task: failed to create shim task: failed to create container f2209bb2960d5162fc9937d3362e1e2cf1724c56d1296ba2551ce510cb2bcd43: guest RPC failure: hostname \"invalid-hostname\\n1.1.1.1 ip6-localhost ip6-loopback localhost\" invalid: must match ^[a-zA-Z0-9_\\-\\.]{0,999}$: unknown" Related work items: #34370598 Closes: https://msazure.visualstudio.com/One/_workitems/edit/34370598 Signed-off-by: Tingmao Wang <tingmaowang@microsoft.com>
1 parent af63d65 commit d2a5931

File tree

4 files changed

+54
-0
lines changed

4 files changed

+54
-0
lines changed

internal/guest/network/network.go

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import (
99
"fmt"
1010
"os"
1111
"path/filepath"
12+
"regexp"
1213
"strings"
1314
"time"
1415

@@ -32,6 +33,18 @@ var (
3233
// maxDNSSearches is limited to 6 in `man 5 resolv.conf`
3334
const maxDNSSearches = 6
3435

36+
var validHostnameRegex = regexp.MustCompile(`^[a-zA-Z0-9_\-\.]{0,255}$`)
37+
38+
// Check that the hostname is safe. This function is less strict than
39+
// technically allowed, but ensures that when the hostname is inserted to
40+
// /etc/hosts, it cannot lead to injection attacks.
41+
func ValidateHostname(hostname string) error {
42+
if !validHostnameRegex.MatchString(hostname) {
43+
return errors.Errorf("hostname %q invalid: must match %s", hostname, validHostnameRegex.String())
44+
}
45+
return nil
46+
}
47+
3548
// GenerateEtcHostsContent generates a /etc/hosts file based on `hostname`.
3649
func GenerateEtcHostsContent(ctx context.Context, hostname string) string {
3750
_, span := oc.StartSpan(ctx, "network::GenerateEtcHostsContent")

internal/guest/network/network_test.go

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import (
77
"context"
88
"os"
99
"path/filepath"
10+
"strings"
1011
"testing"
1112
"time"
1213
)
@@ -122,6 +123,40 @@ func Test_MergeValues(t *testing.T) {
122123
}
123124
}
124125

126+
func Test_ValidateHostname(t *testing.T) {
127+
validNames := []string{
128+
"localhost",
129+
"my-hostname",
130+
"my.hostname",
131+
"my-host-name123",
132+
"_underscores.are.allowed.too",
133+
"", // Allow not specifying a hostname
134+
}
135+
136+
invalidNames := []string{
137+
"localhost\n13.104.0.1 ip6-localhost ip6-loopback localhost",
138+
"localhost\n2603:1000::1 ip6-localhost ip6-loopback",
139+
"hello@microsoft.com",
140+
"has space",
141+
"has,comma",
142+
"\x00",
143+
"a\nb",
144+
strings.Repeat("a", 1000),
145+
}
146+
147+
for _, n := range validNames {
148+
if err := ValidateHostname(n); err != nil {
149+
t.Fatalf("expected %q to be valid, got: %v", n, err)
150+
}
151+
}
152+
153+
for _, n := range invalidNames {
154+
if err := ValidateHostname(n); err == nil {
155+
t.Fatalf("expected %q to be invalid, but got nil error", n)
156+
}
157+
}
158+
}
159+
125160
func Test_GenerateEtcHostsContent(t *testing.T) {
126161
type testcase struct {
127162
name string

internal/guest/runtime/hcsv2/sandbox_container.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,9 @@ func setupSandboxContainerSpec(ctx context.Context, id string, spec *oci.Spec) (
5454

5555
// Write the hostname
5656
hostname := spec.Hostname
57+
if err = network.ValidateHostname(hostname); err != nil {
58+
return err
59+
}
5760
if hostname == "" {
5861
var err error
5962
hostname, err = os.Hostname()

internal/guest/runtime/hcsv2/standalone_container.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,9 @@ func setupStandaloneContainerSpec(ctx context.Context, id string, spec *oci.Spec
6161
}()
6262

6363
hostname := spec.Hostname
64+
if err = network.ValidateHostname(hostname); err != nil {
65+
return err
66+
}
6467
if hostname == "" {
6568
var err error
6669
hostname, err = os.Hostname()

0 commit comments

Comments
 (0)