Skip to content

Commit b55c307

Browse files
committed
Block gateway management loopback
1 parent 89ebd49 commit b55c307

3 files changed

Lines changed: 61 additions & 0 deletions

File tree

internal/proxy/api_key_gateway.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,10 @@ type apiKeyGatewaySpec struct {
4949

5050
func (s Server) rejectMisroutedGatewayCredentials(next http.Handler) http.Handler {
5151
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
52+
if baseURLProbeRequest(r) {
53+
w.WriteHeader(http.StatusNoContent)
54+
return
55+
}
5256
if s.requestUsesConfiguredGatewayCredential(r) {
5357
http.Error(w, "gateway credential used outside its gateway route", http.StatusBadRequest)
5458
return
@@ -150,6 +154,10 @@ func (s Server) apiKeyGatewayHandler(config *APIKeyGatewayConfig, spec apiKeyGat
150154
http.Error(w, spec.name+" gateway upstream path is invalid", http.StatusServiceUnavailable)
151155
return
152156
}
157+
if gatewayPathUsesBlockedPrefix(proxyRequest.URL.Path, "/_subrouter") || gatewayPathUsesBlockedPrefix(joinedUpstreamPath, "/_subrouter") {
158+
http.Error(w, "subrouter management route not allowed", http.StatusForbidden)
159+
return
160+
}
153161
for _, prefix := range spec.blockedPathPrefixes {
154162
if gatewayPathUsesBlockedPrefix(proxyRequest.URL.Path, prefix) || gatewayPathUsesBlockedPrefix(joinedUpstreamPath, prefix) {
155163
http.Error(w, spec.name+" administrative route not allowed", http.StatusForbidden)

internal/proxy/api_key_gateway_test.go

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -772,7 +772,51 @@ func TestRootProxyRejectsMisroutedGatewayCredentials(t *testing.T) {
772772
}
773773
})
774774
}
775+
probe := httptest.NewRequest(http.MethodHead, "/", nil)
776+
probe.Header.Set("X-Goog-Api-Key", "gemini-team")
777+
probeRec := httptest.NewRecorder()
778+
handler.ServeHTTP(probeRec, probe)
779+
if probeRec.Code != http.StatusNoContent {
780+
t.Fatalf("Gemini root probe status = %d body = %s", probeRec.Code, probeRec.Body.String())
781+
}
775782
if got := rootRequests.Load(); got != 0 {
776783
t.Fatalf("misrouted credentials reached root upstream: requests = %d", got)
777784
}
778785
}
786+
787+
func TestConfigurableGatewaysRejectSubrouterManagementPaths(t *testing.T) {
788+
t.Parallel()
789+
790+
var requests atomic.Int32
791+
upstream := httptest.NewServer(http.HandlerFunc(func(http.ResponseWriter, *http.Request) {
792+
requests.Add(1)
793+
}))
794+
defer upstream.Close()
795+
upstreamURL, err := url.Parse(upstream.URL + "/proxy")
796+
if err != nil {
797+
t.Fatal(err)
798+
}
799+
handler := Server{
800+
OpenAIGateway: &APIKeyGatewayConfig{Upstream: upstreamURL, APIKey: "openai-provider", GatewayToken: "openai-team"},
801+
Gemini: &GeminiConfig{Upstream: upstreamURL, APIKey: "gemini-provider", GatewayToken: "gemini-team"},
802+
}.Handler()
803+
tests := []struct {
804+
path string
805+
auth func(http.Header)
806+
}{
807+
{"/openai/_subrouter/drain", func(h http.Header) { h.Set("Authorization", "Bearer openai-team") }},
808+
{"/gemini/_subrouter/transcripts", func(h http.Header) { h.Set("X-Goog-Api-Key", "gemini-team") }},
809+
}
810+
for _, test := range tests {
811+
req := httptest.NewRequest(http.MethodPost, test.path, nil)
812+
test.auth(req.Header)
813+
rec := httptest.NewRecorder()
814+
handler.ServeHTTP(rec, req)
815+
if rec.Code != http.StatusForbidden {
816+
t.Fatalf("%s status = %d body = %s", test.path, rec.Code, rec.Body.String())
817+
}
818+
}
819+
if got := requests.Load(); got != 0 {
820+
t.Fatalf("management requests reached configurable upstream: %d", got)
821+
}
822+
}

internal/proxy/gemini.go

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,15 @@ func (s Server) geminiHandler() http.Handler {
7979
proxyRequest.URL = cloneURL(r.URL)
8080
proxyRequest.URL.Path = stripProviderPathPrefix(proxyRequest.URL.Path, "gemini")
8181
proxyRequest.URL.RawPath = ""
82+
joinedUpstreamPath := joinGatewayUpstreamPath(upstream.Path, proxyRequest.URL.Path)
83+
if gatewayPathIsUnsafe(&url.URL{Path: joinedUpstreamPath}) {
84+
http.Error(w, "gemini gateway upstream path is invalid", http.StatusServiceUnavailable)
85+
return
86+
}
87+
if gatewayPathUsesBlockedPrefix(proxyRequest.URL.Path, "/_subrouter") || gatewayPathUsesBlockedPrefix(joinedUpstreamPath, "/_subrouter") {
88+
http.Error(w, "subrouter management route not allowed", http.StatusForbidden)
89+
return
90+
}
8291
query := proxyRequest.URL.Query()
8392
query.Del("key")
8493
query.Del("api_key")

0 commit comments

Comments
 (0)