Skip to content

Commit ab6a5a6

Browse files
fix(api): reject requests with NUL bytes in URL (#869)
## Summary - Added `NulByteValidationMiddleware` to validate incoming requests - Returns 400 Bad Request when NUL bytes (%00) detected in URL path or query - Prevents PostgreSQL encoding errors from being exposed as 500 errors ## Before ``` GET /v0.1/servers?cursor=%00 → 500 Internal Server Error → "invalid byte sequence for encoding UTF8: 0x00" ``` ## After ``` GET /v0.1/servers?cursor=%00 → 400 Bad Request → "Invalid request: query parameters contain null bytes" ``` Fixes #862 Signed-off-by: majiayu000 <1835304752@qq.com> Co-authored-by: Radoslav Dimitrov <radoslav@stacklok.com>
1 parent 50e030d commit ab6a5a6

File tree

2 files changed

+95
-2
lines changed

2 files changed

+95
-2
lines changed

internal/api/server.go

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,26 @@ import (
1717
"github.com/modelcontextprotocol/registry/internal/telemetry"
1818
)
1919

20+
// NulByteValidationMiddleware rejects requests containing NUL bytes in URL path or query parameters
21+
// This prevents PostgreSQL encoding errors and returns a proper 400 Bad Request
22+
func NulByteValidationMiddleware(next http.Handler) http.Handler {
23+
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
24+
// Check URL path for NUL bytes
25+
if strings.ContainsRune(r.URL.Path, '\x00') {
26+
http.Error(w, "Invalid request: URL path contains null bytes", http.StatusBadRequest)
27+
return
28+
}
29+
30+
// Check raw query string for NUL bytes
31+
if strings.ContainsRune(r.URL.RawQuery, '\x00') {
32+
http.Error(w, "Invalid request: query parameters contain null bytes", http.StatusBadRequest)
33+
return
34+
}
35+
36+
next.ServeHTTP(w, r)
37+
})
38+
}
39+
2040
// TrailingSlashMiddleware redirects requests with trailing slashes to their canonical form
2141
func TrailingSlashMiddleware(next http.Handler) http.Handler {
2242
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
@@ -67,8 +87,8 @@ func NewServer(cfg *config.Config, registryService service.RegistryService, metr
6787
})
6888

6989
// Wrap the mux with middleware stack
70-
// Order: TrailingSlash -> CORS -> Mux
71-
handler := TrailingSlashMiddleware(corsHandler.Handler(mux))
90+
// Order: NulByteValidation -> TrailingSlash -> CORS -> Mux
91+
handler := NulByteValidationMiddleware(TrailingSlashMiddleware(corsHandler.Handler(mux)))
7292

7393
server := &Server{
7494
config: cfg,

internal/api/server_test.go

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,84 @@ package api_test
33
import (
44
"net/http"
55
"net/http/httptest"
6+
"strings"
67
"testing"
78

89
"github.com/modelcontextprotocol/registry/internal/api"
910
)
1011

12+
func TestNulByteValidationMiddleware(t *testing.T) {
13+
// Create a simple handler that returns "OK"
14+
handler := http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
15+
w.WriteHeader(http.StatusOK)
16+
_, _ = w.Write([]byte("OK"))
17+
})
18+
19+
// Wrap with our middleware
20+
middleware := api.NulByteValidationMiddleware(handler)
21+
22+
t.Run("normal path should pass through", func(t *testing.T) {
23+
req := httptest.NewRequest(http.MethodGet, "/v0/servers", nil)
24+
w := httptest.NewRecorder()
25+
middleware.ServeHTTP(w, req)
26+
27+
if w.Code != http.StatusOK {
28+
t.Errorf("expected status %d, got %d", http.StatusOK, w.Code)
29+
}
30+
})
31+
32+
t.Run("path with query params should pass through", func(t *testing.T) {
33+
req := httptest.NewRequest(http.MethodGet, "/v0/servers?cursor=abc123", nil)
34+
w := httptest.NewRecorder()
35+
middleware.ServeHTTP(w, req)
36+
37+
if w.Code != http.StatusOK {
38+
t.Errorf("expected status %d, got %d", http.StatusOK, w.Code)
39+
}
40+
})
41+
42+
t.Run("path with NUL byte should return 400", func(t *testing.T) {
43+
// Create request with NUL byte in path by manually setting URL
44+
req := httptest.NewRequest(http.MethodGet, "/v0/servers/test", nil)
45+
req.URL.Path = "/v0/servers/\x00"
46+
w := httptest.NewRecorder()
47+
middleware.ServeHTTP(w, req)
48+
49+
if w.Code != http.StatusBadRequest {
50+
t.Errorf("expected status %d, got %d", http.StatusBadRequest, w.Code)
51+
}
52+
if !strings.Contains(w.Body.String(), "URL path contains null bytes") {
53+
t.Errorf("expected body to contain error message, got %q", w.Body.String())
54+
}
55+
})
56+
57+
t.Run("query with NUL byte should return 400", func(t *testing.T) {
58+
// Create request with NUL byte in query by manually setting RawQuery
59+
req := httptest.NewRequest(http.MethodGet, "/v0/servers", nil)
60+
req.URL.RawQuery = "cursor=\x00"
61+
w := httptest.NewRecorder()
62+
middleware.ServeHTTP(w, req)
63+
64+
if w.Code != http.StatusBadRequest {
65+
t.Errorf("expected status %d, got %d", http.StatusBadRequest, w.Code)
66+
}
67+
if !strings.Contains(w.Body.String(), "query parameters contain null bytes") {
68+
t.Errorf("expected body to contain error message, got %q", w.Body.String())
69+
}
70+
})
71+
72+
t.Run("path with embedded NUL byte should return 400", func(t *testing.T) {
73+
req := httptest.NewRequest(http.MethodGet, "/v0/servers/test", nil)
74+
req.URL.Path = "/v0/servers/test\x00name"
75+
w := httptest.NewRecorder()
76+
middleware.ServeHTTP(w, req)
77+
78+
if w.Code != http.StatusBadRequest {
79+
t.Errorf("expected status %d, got %d", http.StatusBadRequest, w.Code)
80+
}
81+
})
82+
}
83+
1184
func TestTrailingSlashMiddleware(t *testing.T) {
1285
// Create a simple handler that returns "OK"
1386
handler := http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {

0 commit comments

Comments
 (0)