Skip to content

Commit a7c62f3

Browse files
authored
fix(grpc): sanitize container image refs before containerd (#1071)
* fix(grpc): sanitize container image refs before containerd * chore: allow sanitized spelling in lint config * .golangci.yml: use ignore-words for sanitized * test(grpc): fix sanitizer request factory * test(grpc): align sanitizer expected id
1 parent 427db59 commit a7c62f3

File tree

4 files changed

+177
-2
lines changed

4 files changed

+177
-2
lines changed

.golangci.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@ linters-settings:
2020
line-length: 120
2121
misspell:
2222
locale: UK
23+
ignore-words:
24+
- sanitized
2325
goimports:
2426
local-prefixes: github.com/liquidmetal-dev/flintlock
2527
nolintlint:

infrastructure/grpc/sanitize.go

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
package grpc
2+
3+
import (
4+
"fmt"
5+
"strings"
6+
"unicode"
7+
8+
"github.com/sirupsen/logrus"
9+
10+
"github.com/liquidmetal-dev/flintlock/api/types"
11+
)
12+
13+
func sanitizeMicroVMImageReferences(logger *logrus.Entry, spec *types.MicroVMSpec) {
14+
if spec == nil {
15+
return
16+
}
17+
18+
if spec.Kernel != nil {
19+
sanitizedValue, changed := cleanContainerImageReference(spec.Kernel.Image)
20+
if changed {
21+
logSanitizedField(logger, "kernel.image", spec.Kernel.Image, sanitizedValue)
22+
spec.Kernel.Image = sanitizedValue
23+
}
24+
}
25+
26+
if spec.Initrd != nil {
27+
sanitizedValue, changed := cleanContainerImageReference(spec.Initrd.Image)
28+
if changed {
29+
logSanitizedField(logger, "initrd.image", spec.Initrd.Image, sanitizedValue)
30+
spec.Initrd.Image = sanitizedValue
31+
}
32+
}
33+
34+
sanitizeVolumeImage(logger, "rootVolume", spec.RootVolume)
35+
36+
for index, volume := range spec.AdditionalVolumes {
37+
fieldName := fmt.Sprintf("additionalVolumes[%d]", index)
38+
sanitizeVolumeImage(logger, fieldName, volume)
39+
}
40+
}
41+
42+
func sanitizeVolumeImage(logger *logrus.Entry, fieldName string, volume *types.Volume) {
43+
if volume == nil || volume.Source == nil || volume.Source.ContainerSource == nil {
44+
return
45+
}
46+
47+
originalValue := *volume.Source.ContainerSource
48+
sanitizedValue, changed := cleanContainerImageReference(originalValue)
49+
if !changed {
50+
return
51+
}
52+
53+
logSanitizedField(logger, fieldName+".containerSource", originalValue, sanitizedValue)
54+
volume.Source.ContainerSource = &sanitizedValue
55+
}
56+
57+
func cleanContainerImageReference(raw string) (string, bool) {
58+
if raw == "" {
59+
return raw, false
60+
}
61+
62+
trimmed := strings.TrimSpace(raw)
63+
64+
cleaned := strings.Map(func(r rune) rune {
65+
if r < 0x20 || r == 0x7f {
66+
return -1
67+
}
68+
69+
if unicode.IsControl(r) {
70+
return -1
71+
}
72+
73+
return r
74+
}, trimmed)
75+
76+
if cleaned == raw {
77+
return cleaned, false
78+
}
79+
80+
return cleaned, true
81+
}
82+
83+
func logSanitizedField(logger *logrus.Entry, field string, originalValue, sanitizedValue string) {
84+
logger.WithFields(logrus.Fields{
85+
"field": field,
86+
"originalImage": originalValue,
87+
"sanitizedImage": sanitizedValue,
88+
}).Warn("sanitized container image reference before Flintlock processing")
89+
}

infrastructure/grpc/server.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,8 @@ func (s *server) CreateMicroVM(
4141
//nolint:wrapcheck // don't wrap grpc errors when using the status package
4242
return nil, status.Error(codes.InvalidArgument, "invalid create microvm request: MicroVMSpec required")
4343
}
44+
45+
sanitizeMicroVMImageReferences(logger, req.Microvm)
4446
modelSpec, err := convertMicroVMToModel(req.Microvm)
4547
if err != nil {
4648
return nil, fmt.Errorf("converting request: %w", err)

infrastructure/grpc/server_test.go

Lines changed: 84 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@ func TestServer_CreateMicroVM(t *testing.T) {
2222
createReq *mvm1.CreateMicroVMRequest
2323
expectError bool
2424
expect func(cm *mock.MockMicroVMCommandUseCasesMockRecorder, qm *mock.MockMicroVMQueryUseCasesMockRecorder)
25+
expectedID string
26+
expectedNS string
2527
}{
2628
{
2729
name: "nil request should fail with error",
@@ -58,6 +60,8 @@ func TestServer_CreateMicroVM(t *testing.T) {
5860
name: "valid spec should not fail",
5961
createReq: createTestCreateRequest("mvm1", "default"),
6062
expectError: false,
63+
expectedID: "mvm1",
64+
expectedNS: "default",
6165
expect: func(cm *mock.MockMicroVMCommandUseCasesMockRecorder, qm *mock.MockMicroVMQueryUseCasesMockRecorder) {
6266
vmid, _ := models.NewVMID("mvm1", "default", "uid")
6367

@@ -77,6 +81,80 @@ func TestServer_CreateMicroVM(t *testing.T) {
7781
)
7882
},
7983
},
84+
{
85+
name: "sanitizes control characters before invoking usecase",
86+
createReq: (func() *mvm1.CreateMicroVMRequest {
87+
filename := "kernel"
88+
mac := "AA:FF:00:00:00:01"
89+
rootSource := "\n ghcr.io/test/root:latest\r"
90+
additionalSource := "\tghcr.io/test/additional:latest\n"
91+
initrdImage := "\n ghcr.io/test/initrd:latest\n"
92+
93+
return &mvm1.CreateMicroVMRequest{
94+
Microvm: &types.MicroVMSpec{
95+
Id: "mvm-sanitize",
96+
Namespace: "default",
97+
Vcpu: 2,
98+
MemoryInMb: 1024,
99+
Kernel: &types.Kernel{
100+
Image: "\n ghcr.io/test/kernel:latest\t",
101+
Filename: &filename,
102+
},
103+
Initrd: &types.Initrd{
104+
Image: initrdImage,
105+
},
106+
Interfaces: []*types.NetworkInterface{
107+
{
108+
DeviceId: "eth0",
109+
GuestMac: &mac,
110+
Type: types.NetworkInterface_MACVTAP,
111+
},
112+
},
113+
RootVolume: &types.Volume{
114+
Id: "root",
115+
Source: &types.VolumeSource{
116+
ContainerSource: &rootSource,
117+
},
118+
},
119+
AdditionalVolumes: []*types.Volume{
120+
{
121+
Id: "extra",
122+
Source: &types.VolumeSource{
123+
ContainerSource: &additionalSource,
124+
},
125+
},
126+
},
127+
},
128+
}
129+
})(),
130+
expectError: false,
131+
expectedID: "mvm-sanitize",
132+
expectedNS: "default",
133+
expect: func(cm *mock.MockMicroVMCommandUseCasesMockRecorder, qm *mock.MockMicroVMQueryUseCasesMockRecorder) {
134+
vmid, _ := models.NewVMID("mvm-sanitize", "default", "uid")
135+
cm.CreateMicroVM(
136+
gomock.AssignableToTypeOf(context.Background()),
137+
gomock.AssignableToTypeOf(&models.MicroVM{}),
138+
).DoAndReturn(func(_ context.Context, mvm *models.MicroVM) (*models.MicroVM, error) {
139+
Expect(string(mvm.Spec.Kernel.Image)).To(Equal("ghcr.io/test/kernel:latest"))
140+
Expect(mvm.Spec.Initrd).NotTo(BeNil())
141+
Expect(string(mvm.Spec.Initrd.Image)).To(Equal("ghcr.io/test/initrd:latest"))
142+
Expect(mvm.Spec.RootVolume.Source.Container).NotTo(BeNil())
143+
Expect(string(mvm.Spec.RootVolume.Source.Container.Image)).To(Equal("ghcr.io/test/root:latest"))
144+
Expect(len(mvm.Spec.AdditionalVolumes)).To(Equal(1))
145+
Expect(string(mvm.Spec.AdditionalVolumes[0].Source.Container.Image)).To(Equal("ghcr.io/test/additional:latest"))
146+
147+
return &models.MicroVM{
148+
ID: *vmid,
149+
Version: 0,
150+
Spec: mvm.Spec,
151+
Status: models.MicroVMStatus{
152+
State: models.CreatedState,
153+
},
154+
}, nil
155+
})
156+
},
157+
},
80158
}
81159

82160
for _, tc := range tt {
@@ -97,8 +175,12 @@ func TestServer_CreateMicroVM(t *testing.T) {
97175
Expect(err).To(HaveOccurred())
98176
} else {
99177
Expect(err).NotTo(HaveOccurred())
100-
Expect(resp.Microvm.Spec.Id).To(Equal("mvm1"))
101-
Expect(resp.Microvm.Spec.Namespace).To(Equal("default"))
178+
if tc.expectedID != "" {
179+
Expect(resp.Microvm.Spec.Id).To(Equal(tc.expectedID))
180+
}
181+
if tc.expectedNS != "" {
182+
Expect(resp.Microvm.Spec.Namespace).To(Equal(tc.expectedNS))
183+
}
102184
}
103185
})
104186
}

0 commit comments

Comments
 (0)