mirror of
https://github.com/netbirdio/netbird.git
synced 2026-05-29 20:19:56 +00:00
fix(rest): reject empty Delete path params in reverse-proxy clients
ReverseProxyClustersAPI.Delete and ReverseProxyTokensAPI.Delete passed
the path parameter into url.PathEscape without an empty check.
PathEscape("") returns "" which collapses the request onto the
collection endpoint ("/api/reverse-proxies/clusters/" /
"/api/reverse-proxies/proxy-tokens/"), so a caller bug delete with no
id reached a routable URL with surprising semantics (typically 405).
Short-circuit with a typed error before the request is built. Tests
mount a handler on the collection path that fails the test if hit, so
the regression is impossible to reintroduce silently.
This commit is contained in:
@@ -2,6 +2,7 @@ package rest
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"net/url"
|
||||
|
||||
"github.com/netbirdio/netbird/shared/management/http/api"
|
||||
@@ -33,6 +34,12 @@ func (a *ReverseProxyClustersAPI) List(ctx context.Context) ([]api.ProxyCluster,
|
||||
// NetBird cannot be deleted via this endpoint; the server returns 404 / 400
|
||||
// for cluster addresses the account does not own.
|
||||
func (a *ReverseProxyClustersAPI) Delete(ctx context.Context, clusterAddress string) error {
|
||||
// Guard against the empty input: url.PathEscape("") returns "" which
|
||||
// would collapse the request URL onto the collection endpoint and
|
||||
// silently delete nothing (or 405 depending on routing).
|
||||
if clusterAddress == "" {
|
||||
return errors.New("clusterAddress is required")
|
||||
}
|
||||
resp, err := a.c.NewRequest(ctx, "DELETE", "/api/reverse-proxies/clusters/"+url.PathEscape(clusterAddress), nil, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
|
||||
@@ -88,3 +88,17 @@ func TestReverseProxyClusters_Delete_Err(t *testing.T) {
|
||||
assert.Error(t, err)
|
||||
})
|
||||
}
|
||||
|
||||
// TestReverseProxyClusters_Delete_EmptyAddress guards against an empty
|
||||
// clusterAddress reaching the wire — that would collapse the URL onto
|
||||
// the collection endpoint instead of a specific cluster. The client
|
||||
// must short-circuit with a typed error before any request is issued.
|
||||
func TestReverseProxyClusters_Delete_EmptyAddress(t *testing.T) {
|
||||
withMockClient(func(c *rest.Client, mux *http.ServeMux) {
|
||||
mux.HandleFunc("/api/reverse-proxies/clusters/", func(http.ResponseWriter, *http.Request) {
|
||||
t.Fatal("empty clusterAddress must be rejected client-side; no request should reach the server")
|
||||
})
|
||||
err := c.ReverseProxyClusters.Delete(context.Background(), "")
|
||||
assert.Error(t, err, "empty clusterAddress must surface as an error")
|
||||
})
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"net/url"
|
||||
|
||||
"github.com/netbirdio/netbird/shared/management/http/api"
|
||||
@@ -61,6 +62,12 @@ func (a *ReverseProxyTokensAPI) Create(ctx context.Context, request api.ProxyTok
|
||||
// credentials existed; the plain secret can no longer authenticate any
|
||||
// new proxy registration.
|
||||
func (a *ReverseProxyTokensAPI) Delete(ctx context.Context, tokenID string) error {
|
||||
// Guard against the empty input: url.PathEscape("") returns "" which
|
||||
// would collapse the request URL onto the collection endpoint and
|
||||
// silently delete nothing (or 405 depending on routing).
|
||||
if tokenID == "" {
|
||||
return errors.New("tokenID is required")
|
||||
}
|
||||
resp, err := a.c.NewRequest(ctx, "DELETE", "/api/reverse-proxies/proxy-tokens/"+url.PathEscape(tokenID), nil, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
|
||||
@@ -129,3 +129,16 @@ func TestReverseProxyTokens_Delete_Err(t *testing.T) {
|
||||
assert.Error(t, err)
|
||||
})
|
||||
}
|
||||
|
||||
// TestReverseProxyTokens_Delete_EmptyID guards against an empty tokenID
|
||||
// reaching the wire — url.PathEscape("") would collapse the URL onto
|
||||
// the collection endpoint.
|
||||
func TestReverseProxyTokens_Delete_EmptyID(t *testing.T) {
|
||||
withMockClient(func(c *rest.Client, mux *http.ServeMux) {
|
||||
mux.HandleFunc("/api/reverse-proxies/proxy-tokens/", func(http.ResponseWriter, *http.Request) {
|
||||
t.Fatal("empty tokenID must be rejected client-side; no request should reach the server")
|
||||
})
|
||||
err := c.ReverseProxyTokens.Delete(context.Background(), "")
|
||||
assert.Error(t, err, "empty tokenID must surface as an error")
|
||||
})
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user