Skip to content

Commit 9fe3583

Browse files
committed
fixup! fixup! fix
Signed-off-by: Oleksii Kurinnyi <[email protected]>
1 parent 09c5bdc commit 9fe3583

File tree

3 files changed

+191
-25
lines changed

3 files changed

+191
-25
lines changed

controllers/workspace/devworkspace_controller.go

Lines changed: 88 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import (
2222
"strconv"
2323
"strings"
2424
"time"
25+
"unicode/utf8"
2526

2627
"github.com/devfile/devworkspace-operator/pkg/library/initcontainers"
2728
"github.com/devfile/devworkspace-operator/pkg/library/ssh"
@@ -69,6 +70,16 @@ import (
6970
"sigs.k8s.io/controller-runtime/pkg/reconcile"
7071
)
7172

73+
const (
74+
// Validation limits for init-persistent-home container Command field
75+
maxCommandElements = 10
76+
maxCommandElementLength = 1024
77+
78+
// Validation limits for init-persistent-home container Args field
79+
maxArgsElements = 50
80+
maxArgsElementLength = 256 * 1024 // 256KB
81+
)
82+
7283
// validateNoAdvancedFields validates that the init-persistent-home container
7384
// does not use advanced Kubernetes container fields that could make behavior unpredictable.
7485
func validateNoAdvancedFields(c corev1.Container) error {
@@ -121,17 +132,17 @@ func validateHomeInitContainer(c corev1.Container) error {
121132
}
122133
}
123134

124-
// Only validate if present
125-
if c.Command != nil {
126-
if len(c.Command) != 2 || c.Command[0] != "/bin/sh" || c.Command[1] != "-c" {
127-
return fmt.Errorf("command must be exactly [/bin/sh, -c] for %s", constants.HomeInitComponentName)
135+
// Validate Command for security and resource limits (only if present)
136+
if len(c.Command) > 0 {
137+
if err := validateCommand(c.Command); err != nil {
138+
return fmt.Errorf("invalid command for %s: %w", constants.HomeInitComponentName, err)
128139
}
129140
}
130141

131-
// Only validate if present
132-
if c.Args != nil {
133-
if len(c.Args) != 1 {
134-
return fmt.Errorf("args must contain exactly one script string for %s", constants.HomeInitComponentName)
142+
// Validate Args for security and resource limits (only if present)
143+
if len(c.Args) > 0 {
144+
if err := validateArgs(c.Args); err != nil {
145+
return fmt.Errorf("invalid args for %s: %w", constants.HomeInitComponentName, err)
135146
}
136147
}
137148

@@ -192,13 +203,79 @@ func validateImageReference(image string) error {
192203
return nil
193204
}
194205

206+
// validateCommand validates the Command field for the init-persistent-home container.
207+
// This validation prevents resource exhaustion and data corruption while allowing flexibility
208+
// for enterprise customization.
209+
// Generated by Claude Sonnet 4.5
210+
func validateCommand(command []string) error {
211+
if len(command) > maxCommandElements {
212+
return fmt.Errorf("command cannot exceed %d elements (got %d)", maxCommandElements, len(command))
213+
}
214+
for i, cmd := range command {
215+
if len(cmd) > maxCommandElementLength {
216+
return fmt.Errorf("command[%d] exceeds %d characters (got %d)", i, maxCommandElementLength, len(cmd))
217+
}
218+
if err := validateStringContent(cmd, fmt.Sprintf("command[%d]", i)); err != nil {
219+
return err
220+
}
221+
}
222+
return nil
223+
}
224+
225+
// validateArgs validates the Args field for the init-persistent-home container.
226+
// This validation prevents resource exhaustion and data corruption while allowing flexibility
227+
// for enterprise customization.
228+
// Generated by Claude Sonnet 4.5
229+
func validateArgs(args []string) error {
230+
if len(args) > maxArgsElements {
231+
return fmt.Errorf("args cannot exceed %d elements (got %d)", maxArgsElements, len(args))
232+
}
233+
for i, arg := range args {
234+
if len(arg) > maxArgsElementLength {
235+
return fmt.Errorf("args[%d] exceeds %d bytes (got %d bytes)", i, maxArgsElementLength, len(arg))
236+
}
237+
if err := validateStringContent(arg, fmt.Sprintf("args[%d]", i)); err != nil {
238+
return err
239+
}
240+
}
241+
return nil
242+
}
243+
244+
// validateStringContent validates the content of command/args strings to prevent
245+
// data corruption and ensure valid UTF-8 encoding.
246+
// Generated by Claude Sonnet 4.5
247+
func validateStringContent(s, fieldName string) error {
248+
// No null bytes (breaks JSON marshaling and container runtime)
249+
if strings.Contains(s, "\x00") {
250+
return fmt.Errorf("%s contains null bytes", fieldName)
251+
}
252+
253+
// Check for invalid control characters (allow \n, \t which are common in scripts)
254+
for _, r := range s {
255+
if r < 0x20 && r != '\n' && r != '\t' {
256+
return fmt.Errorf("%s contains invalid control character (U+%04X)", fieldName, r)
257+
}
258+
if r == 0x7F {
259+
return fmt.Errorf("%s contains DEL control character", fieldName)
260+
}
261+
}
262+
263+
// Must be valid UTF-8
264+
if !utf8.ValidString(s) {
265+
return fmt.Errorf("%s contains invalid UTF-8", fieldName)
266+
}
267+
268+
return nil
269+
}
270+
195271
// ensureHomeInitContainerFields ensures that an init-persistent-home container has
196272
// the correct Command, Args, and VolumeMounts.
197273
func ensureHomeInitContainerFields(c *corev1.Container) error {
198-
c.Command = []string{"/bin/sh", "-c"}
199-
if len(c.Args) != 1 {
200-
return fmt.Errorf("args must contain exactly one script string for %s", constants.HomeInitComponentName)
274+
// Set default command only if not provided
275+
if len(c.Command) == 0 {
276+
c.Command = []string{"/bin/sh", "-c"}
201277
}
278+
// Args are validated separately in validateCommandAndArgs
202279
c.VolumeMounts = []corev1.VolumeMount{{
203280
Name: constants.HomeVolumeName,
204281
MountPath: constants.HomeUserDirectory,

controllers/workspace/init_container_validation_test.go

Lines changed: 101 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -59,46 +59,42 @@ func TestValidateInitContainer(t *testing.T) {
5959
expectError: false,
6060
},
6161
{
62-
name: "Rejects invalid command",
62+
name: "Accepts custom command",
6363
container: corev1.Container{
6464
Name: constants.HomeInitComponentName,
6565
Image: "custom-image:latest",
6666
Command: []string{"/bin/sh"},
6767
Args: []string{"echo 'test'"},
6868
},
69-
expectError: true,
70-
errorMsg: "command must be exactly [/bin/sh, -c]",
69+
expectError: false,
7170
},
7271
{
73-
name: "Rejects empty command",
72+
name: "Accepts empty command",
7473
container: corev1.Container{
7574
Name: constants.HomeInitComponentName,
7675
Image: "custom-image:latest",
7776
Command: []string{},
7877
Args: []string{"echo 'test'"},
7978
},
80-
expectError: true,
81-
errorMsg: "command must be exactly [/bin/sh, -c]",
79+
expectError: false,
8280
},
8381
{
84-
name: "Rejects empty args",
82+
name: "Accepts empty args",
8583
container: corev1.Container{
8684
Name: constants.HomeInitComponentName,
8785
Image: "custom-image:latest",
8886
Args: []string{},
8987
},
90-
expectError: true,
91-
errorMsg: "args must contain exactly one script string",
88+
expectError: false,
9289
},
9390
{
94-
name: "Rejects multiple args",
91+
name: "Accepts multiple args",
9592
container: corev1.Container{
9693
Name: constants.HomeInitComponentName,
9794
Image: "custom-image:latest",
9895
Args: []string{"echo 'test'", "echo 'test2'"},
9996
},
100-
expectError: true,
101-
errorMsg: "args must contain exactly one script string",
97+
expectError: false,
10298
},
10399
{
104100
name: "Rejects user-provided volumeMounts",
@@ -267,3 +263,96 @@ func TestValidateImageReference(t *testing.T) {
267263
})
268264
}
269265
}
266+
267+
// makeStringSlice creates a string slice of the specified length with the given value.
268+
// Generated by Claude Sonnet 4.5
269+
func makeStringSlice(count int, value string) []string {
270+
result := make([]string, count)
271+
for i := range count {
272+
result[i] = value
273+
}
274+
return result
275+
}
276+
277+
// TestValidateCommand tests validation of Command field.
278+
// Generated by Claude Sonnet 4.5
279+
func TestValidateCommand(t *testing.T) {
280+
tests := []struct {
281+
name string
282+
command []string
283+
expectError bool
284+
errorMsg string
285+
}{
286+
// Valid cases
287+
{"Valid command", []string{"/bin/sh", "-c"}, false, ""},
288+
{"Empty command", []string{}, false, ""},
289+
{"Command with newlines and tabs", []string{"/bin/sh\ntest\ttab"}, false, ""},
290+
{"Max valid elements", makeStringSlice(maxCommandElements, "x"), false, ""},
291+
{"Max valid element length", []string{strings.Repeat("a", maxCommandElementLength)}, false, ""},
292+
{"Valid UTF-8 with emoji", []string{"/bin/sh", "🚀"}, false, ""},
293+
294+
// Invalid cases
295+
{"Too many elements", makeStringSlice(maxCommandElements+1, "x"), true, "command cannot exceed"},
296+
{"Element too long", []string{strings.Repeat("a", maxCommandElementLength+1)}, true, "command[0] exceeds"},
297+
{"Null byte", []string{"/bin/sh\x00malicious"}, true, "contains null bytes"},
298+
{"Control character", []string{"/bin/sh\x01"}, true, "contains invalid control character"},
299+
{"DEL character", []string{"/bin/sh\x7F"}, true, "contains DEL control character"},
300+
{"Invalid UTF-8", []string{string([]byte{0xFF, 0xFE, 0xFD})}, true, "contains invalid UTF-8"},
301+
}
302+
303+
for _, tt := range tests {
304+
t.Run(tt.name, func(t *testing.T) {
305+
err := validateCommand(tt.command)
306+
307+
if tt.expectError {
308+
assert.Error(t, err)
309+
if tt.errorMsg != "" {
310+
assert.Contains(t, err.Error(), tt.errorMsg)
311+
}
312+
} else {
313+
assert.NoError(t, err)
314+
}
315+
})
316+
}
317+
}
318+
319+
// TestValidateArgs tests validation of Args field.
320+
// Generated by Claude Sonnet 4.5
321+
func TestValidateArgs(t *testing.T) {
322+
tests := []struct {
323+
name string
324+
args []string
325+
expectError bool
326+
errorMsg string
327+
}{
328+
// Valid cases
329+
{"Valid args", []string{"echo 'hello'"}, false, ""},
330+
{"Empty args", []string{}, false, ""},
331+
{"Args with newlines and tabs", []string{"#!/bin/sh\necho 'test'\n\techo 'indented'"}, false, ""},
332+
{"Max valid elements", make([]string, maxArgsElements), false, ""},
333+
{"Max valid element length", []string{strings.Repeat("a", maxArgsElementLength)}, false, ""},
334+
{"Valid UTF-8 with emoji", []string{"echo '🚀 Hello World 你好'"}, false, ""},
335+
336+
// Invalid cases
337+
{"Too many elements", make([]string, maxArgsElements+1), true, "args cannot exceed"},
338+
{"Element too large", []string{strings.Repeat("a", maxArgsElementLength+1)}, true, "args[0] exceeds"},
339+
{"Null byte", []string{"echo\x00malicious"}, true, "contains null bytes"},
340+
{"Control character", []string{"echo\x02test"}, true, "contains invalid control character"},
341+
{"Invalid UTF-8", []string{string([]byte{0xFF, 0xFE})}, true, "contains invalid UTF-8"},
342+
}
343+
344+
for _, tt := range tests {
345+
t.Run(tt.name, func(t *testing.T) {
346+
err := validateArgs(tt.args)
347+
348+
if tt.expectError {
349+
assert.Error(t, err)
350+
if tt.errorMsg != "" {
351+
assert.Contains(t, err.Error(), tt.errorMsg)
352+
}
353+
} else {
354+
assert.NoError(t, err)
355+
}
356+
})
357+
}
358+
}

docs/dwo-configuration.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -167,8 +167,8 @@ A specially-named init container `init-persistent-home` can be used to override
167167

168168
- **Name:** Must be exactly `init-persistent-home`
169169
- **Image:** Optional. If omitted, defaults to the first non-imported workspace container's image. If no suitable image can be inferred, the workspace will fail to start with an error.
170-
- **Command:** Optional. If omitted, defaults to `["/bin/sh", "-c"]`. If provided, must exactly match this value.
171-
- **Args:** Required. Must contain exactly one string with the initialization script.
170+
- **Command:** Optional. If omitted, defaults to `["/bin/sh", "-c"]`. If provided, can be any valid command array.
171+
- **Args:** Optional. If omitted and command is also omitted, defaults to a single script string. If provided, can be any valid args array.
172172
- **VolumeMounts:** Forbidden. The operator automatically mounts the `persistent-home` volume at `/home/user/`.
173173
- **Env:** Optional. Environment variables are allowed.
174174
- **Other fields:** Not allowed. Fields such as `ports`, `probes`, `lifecycle`, `securityContext`, `resources`, `volumeDevices`, `stdin`, `tty`, and `workingDir` are rejected to keep behavior predictable.

0 commit comments

Comments
 (0)