Skip to content

Commit c9ae434

Browse files
committed
fix: validate server names to allow only single slash (#471)
1 parent 05003c5 commit c9ae434

File tree

4 files changed

+343
-5
lines changed

4 files changed

+343
-5
lines changed

internal/api/handlers/v0/publish_test.go

Lines changed: 231 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -224,6 +224,135 @@ func TestPublishEndpoint(t *testing.T) {
224224
setupRegistryService: func(_ service.RegistryService) {},
225225
expectedStatus: http.StatusOK,
226226
},
227+
// IB-2-registry: Integration test for multi-slash server name rejection
228+
{
229+
name: "invalid server name - multiple slashes (two slashes)",
230+
requestBody: apiv0.ServerJSON{
231+
Name: "com.example/server/path",
232+
Description: "Server with multiple slashes in name",
233+
Version: "1.0.0",
234+
Repository: model.Repository{
235+
URL: "https://github.com/example/test-server",
236+
Source: "github",
237+
ID: "example/test-server",
238+
},
239+
},
240+
tokenClaims: &auth.JWTClaims{
241+
AuthMethod: auth.MethodNone,
242+
Permissions: []auth.Permission{
243+
{Action: auth.PermissionActionPublish, ResourcePattern: "*"},
244+
},
245+
},
246+
setupRegistryService: func(_ service.RegistryService) {},
247+
expectedStatus: http.StatusBadRequest,
248+
expectedError: "server name cannot contain multiple slashes",
249+
},
250+
{
251+
name: "invalid server name - multiple slashes (three slashes)",
252+
requestBody: apiv0.ServerJSON{
253+
Name: "org.company/dept/team/project",
254+
Description: "Server with three slashes in name",
255+
Version: "1.0.0",
256+
},
257+
tokenClaims: &auth.JWTClaims{
258+
AuthMethod: auth.MethodNone,
259+
Permissions: []auth.Permission{
260+
{Action: auth.PermissionActionPublish, ResourcePattern: "*"},
261+
},
262+
},
263+
setupRegistryService: func(_ service.RegistryService) {},
264+
expectedStatus: http.StatusBadRequest,
265+
expectedError: "server name cannot contain multiple slashes",
266+
},
267+
{
268+
name: "invalid server name - consecutive slashes",
269+
requestBody: apiv0.ServerJSON{
270+
Name: "com.example//double-slash",
271+
Description: "Server with consecutive slashes",
272+
Version: "1.0.0",
273+
},
274+
tokenClaims: &auth.JWTClaims{
275+
AuthMethod: auth.MethodNone,
276+
Permissions: []auth.Permission{
277+
{Action: auth.PermissionActionPublish, ResourcePattern: "*"},
278+
},
279+
},
280+
setupRegistryService: func(_ service.RegistryService) {},
281+
expectedStatus: http.StatusBadRequest,
282+
expectedError: "server name cannot contain multiple slashes",
283+
},
284+
{
285+
name: "invalid server name - URL-like path",
286+
requestBody: apiv0.ServerJSON{
287+
Name: "com.example/servers/v1/api",
288+
Description: "Server with URL-like path structure",
289+
Version: "1.0.0",
290+
},
291+
tokenClaims: &auth.JWTClaims{
292+
AuthMethod: auth.MethodNone,
293+
Permissions: []auth.Permission{
294+
{Action: auth.PermissionActionPublish, ResourcePattern: "*"},
295+
},
296+
},
297+
setupRegistryService: func(_ service.RegistryService) {},
298+
expectedStatus: http.StatusBadRequest,
299+
expectedError: "server name cannot contain multiple slashes",
300+
},
301+
{
302+
name: "invalid server name - many slashes",
303+
requestBody: apiv0.ServerJSON{
304+
Name: "a/b/c/d/e/f",
305+
Description: "Server with many slashes",
306+
Version: "1.0.0",
307+
},
308+
tokenClaims: &auth.JWTClaims{
309+
AuthMethod: auth.MethodNone,
310+
Permissions: []auth.Permission{
311+
{Action: auth.PermissionActionPublish, ResourcePattern: "*"},
312+
},
313+
},
314+
setupRegistryService: func(_ service.RegistryService) {},
315+
expectedStatus: http.StatusBadRequest,
316+
expectedError: "server name cannot contain multiple slashes",
317+
},
318+
{
319+
name: "invalid server name - with packages and remotes",
320+
requestBody: apiv0.ServerJSON{
321+
Name: "com.example/test/server/v2",
322+
Description: "Complex server with invalid name",
323+
Version: "2.0.0",
324+
Repository: model.Repository{
325+
URL: "https://github.com/example/test-server",
326+
Source: "github",
327+
ID: "example/test-server",
328+
},
329+
Packages: []model.Package{
330+
{
331+
RegistryType: model.RegistryTypeNPM,
332+
Identifier: "test-package",
333+
Version: "2.0.0",
334+
Transport: model.Transport{
335+
Type: model.TransportTypeStdio,
336+
},
337+
},
338+
},
339+
Remotes: []model.Transport{
340+
{
341+
Type: model.TransportTypeStreamableHTTP,
342+
URL: "https://example.com/api",
343+
},
344+
},
345+
},
346+
tokenClaims: &auth.JWTClaims{
347+
AuthMethod: auth.MethodNone,
348+
Permissions: []auth.Permission{
349+
{Action: auth.PermissionActionPublish, ResourcePattern: "*"},
350+
},
351+
},
352+
setupRegistryService: func(_ service.RegistryService) {},
353+
expectedStatus: http.StatusBadRequest,
354+
expectedError: "server name cannot contain multiple slashes",
355+
},
227356
}
228357

229358
for _, tc := range testCases {
@@ -279,3 +408,105 @@ func TestPublishEndpoint(t *testing.T) {
279408
})
280409
}
281410
}
411+
412+
// TestPublishEndpoint_MultipleSlashesEdgeCases tests additional edge cases for multi-slash validation
413+
func TestPublishEndpoint_MultipleSlashesEdgeCases(t *testing.T) {
414+
testSeed := make([]byte, ed25519.SeedSize)
415+
_, err := rand.Read(testSeed)
416+
require.NoError(t, err)
417+
testConfig := &config.Config{
418+
JWTPrivateKey: hex.EncodeToString(testSeed),
419+
EnableRegistryValidation: false,
420+
}
421+
422+
testCases := []struct {
423+
name string
424+
serverName string
425+
expectedStatus int
426+
description string
427+
}{
428+
{
429+
name: "valid - single slash",
430+
serverName: "com.example/server",
431+
expectedStatus: http.StatusOK,
432+
description: "Valid server name with single slash should succeed",
433+
},
434+
{
435+
name: "invalid - trailing slash after valid name",
436+
serverName: "com.example/server/",
437+
expectedStatus: http.StatusBadRequest,
438+
description: "Trailing slash creates multiple slashes",
439+
},
440+
{
441+
name: "invalid - leading and middle slash",
442+
serverName: "/com.example/server",
443+
expectedStatus: http.StatusBadRequest,
444+
description: "Leading slash with middle slash",
445+
},
446+
{
447+
name: "invalid - file system style path",
448+
serverName: "usr/local/bin/server",
449+
expectedStatus: http.StatusBadRequest,
450+
description: "File system style paths should be rejected",
451+
},
452+
{
453+
name: "invalid - version-like suffix",
454+
serverName: "com.example/server/v1.0.0",
455+
expectedStatus: http.StatusBadRequest,
456+
description: "Version suffixes with slash should be rejected",
457+
},
458+
}
459+
460+
for _, tc := range testCases {
461+
t.Run(tc.name, func(t *testing.T) {
462+
// Create registry service
463+
registryService := service.NewRegistryService(database.NewMemoryDB(), testConfig)
464+
465+
// Create a new ServeMux and Huma API
466+
mux := http.NewServeMux()
467+
api := humago.New(mux, huma.DefaultConfig("Test API", "1.0.0"))
468+
469+
// Register the endpoint
470+
v0.RegisterPublishEndpoint(api, registryService, testConfig)
471+
472+
// Create request body
473+
requestBody := apiv0.ServerJSON{
474+
Name: tc.serverName,
475+
Description: "Test server",
476+
Version: "1.0.0",
477+
}
478+
479+
bodyBytes, err := json.Marshal(requestBody)
480+
require.NoError(t, err)
481+
482+
// Create request
483+
req, err := http.NewRequestWithContext(context.Background(), http.MethodPost, "/v0/publish", bytes.NewBuffer(bodyBytes))
484+
require.NoError(t, err)
485+
req.Header.Set("Content-Type", "application/json")
486+
487+
// Set auth header with permissions
488+
tokenClaims := auth.JWTClaims{
489+
AuthMethod: auth.MethodNone,
490+
Permissions: []auth.Permission{
491+
{Action: auth.PermissionActionPublish, ResourcePattern: "*"},
492+
},
493+
}
494+
token, err := generateTestJWTToken(testConfig, tokenClaims)
495+
require.NoError(t, err)
496+
req.Header.Set("Authorization", "Bearer "+token)
497+
498+
// Perform request
499+
rr := httptest.NewRecorder()
500+
mux.ServeHTTP(rr, req)
501+
502+
// Assertions
503+
assert.Equal(t, tc.expectedStatus, rr.Code,
504+
"%s: expected status %d, got %d", tc.description, tc.expectedStatus, rr.Code)
505+
506+
if tc.expectedStatus == http.StatusBadRequest {
507+
assert.Contains(t, rr.Body.String(), "server name cannot contain multiple slashes",
508+
"%s: should contain specific error message", tc.description)
509+
}
510+
})
511+
}
512+
}

internal/validators/constants.go

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,9 @@ var (
2525
ErrInvalidNamedArgumentName = errors.New("invalid named argument name format")
2626
ErrArgumentValueStartsWithName = errors.New("argument value cannot start with the argument name")
2727
ErrArgumentDefaultStartsWithName = errors.New("argument default cannot start with the argument name")
28+
29+
// Server name validation errors
30+
ErrMultipleSlashesInServerName = errors.New("server name cannot contain multiple slashes")
2831
)
2932

3033
// RepositorySource represents valid repository sources
@@ -33,4 +36,4 @@ type RepositorySource string
3336
const (
3437
SourceGitHub RepositorySource = "github"
3538
SourceGitLab RepositorySource = "gitlab"
36-
)
39+
)

internal/validators/validators.go

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -402,6 +402,12 @@ func parseServerName(serverJSON apiv0.ServerJSON) (string, error) {
402402
return "", fmt.Errorf("server name must be in format 'dns-namespace/name' (e.g., 'com.example.api/server')")
403403
}
404404

405+
// Check for multiple slashes - reject if found
406+
slashCount := strings.Count(name, "/")
407+
if slashCount > 1 {
408+
return "", ErrMultipleSlashesInServerName
409+
}
410+
405411
parts := strings.SplitN(name, "/", 2)
406412
if len(parts) != 2 || parts[0] == "" || parts[1] == "" {
407413
return "", fmt.Errorf("server name must be in format 'dns-namespace/name' with non-empty namespace and name parts")
@@ -504,4 +510,4 @@ func isValidHostForDomain(hostname, publisherDomain string) bool {
504510
}
505511

506512
return false
507-
}
513+
}

0 commit comments

Comments
 (0)