@@ -14,6 +14,7 @@ import (
1414 "net/url"
1515 "os"
1616 "path/filepath"
17+ "strings"
1718 "sync/atomic"
1819 "testing"
1920 "time"
@@ -2635,6 +2636,78 @@ func TestUIEndpoint_MetricsProxy(t *testing.T) {
26352636 wantCode : http .StatusMovedPermanently ,
26362637 wantContains : "Moved Permanently" ,
26372638 },
2639+ {
2640+ name : "path traversal with single dot-dot should be cleaned" ,
2641+ config : config.UIMetricsProxy {
2642+ BaseURL : backendURL ,
2643+ },
2644+ path : endpointPath + "/../ok" ,
2645+ wantCode : http .StatusMovedPermanently ,
2646+ wantContains : "Moved Permanently" ,
2647+ },
2648+ {
2649+ name : "path traversal with multiple dot-dots should be cleaned" ,
2650+ config : config.UIMetricsProxy {
2651+ BaseURL : backendURL ,
2652+ },
2653+ path : endpointPath + "/../../ok" ,
2654+ wantCode : http .StatusMovedPermanently ,
2655+ wantContains : "Moved Permanently" ,
2656+ },
2657+ {
2658+ name : "path traversal with mixed slashes should be cleaned" ,
2659+ config : config.UIMetricsProxy {
2660+ BaseURL : backendURL ,
2661+ },
2662+ path : endpointPath + "/./../ok" ,
2663+ wantCode : http .StatusMovedPermanently ,
2664+ wantContains : "Moved Permanently" ,
2665+ },
2666+ {
2667+ name : "path traversal with encoded dots should be handled" ,
2668+ config : config.UIMetricsProxy {
2669+ BaseURL : backendURL ,
2670+ },
2671+ path : endpointPath + "/%2e%2e/ok" ,
2672+ wantCode : http .StatusOK ,
2673+ wantContains : "OK" ,
2674+ },
2675+ {
2676+ name : "path with double slashes should be cleaned" ,
2677+ config : config.UIMetricsProxy {
2678+ BaseURL : backendURL ,
2679+ },
2680+ path : endpointPath + "//ok" ,
2681+ wantCode : http .StatusMovedPermanently ,
2682+ wantContains : "Moved Permanently" ,
2683+ },
2684+ {
2685+ name : "path with trailing slash should work" ,
2686+ config : config.UIMetricsProxy {
2687+ BaseURL : backendURL ,
2688+ },
2689+ path : endpointPath + "/ok/" ,
2690+ wantCode : http .StatusOK ,
2691+ wantContains : "OK" ,
2692+ },
2693+ {
2694+ name : "baseURL exact match should work" ,
2695+ config : config.UIMetricsProxy {
2696+ BaseURL : strings .TrimSuffix (backendURL , "/" ),
2697+ },
2698+ path : endpointPath + "/" ,
2699+ wantCode : http .StatusNotFound ,
2700+ wantContains : "not found on backend" ,
2701+ },
2702+ {
2703+ name : "clean path prevents directory traversal" ,
2704+ config : config.UIMetricsProxy {
2705+ BaseURL : backendURL ,
2706+ },
2707+ path : endpointPath + "/subdir/../../../etc/passwd" ,
2708+ wantCode : http .StatusMovedPermanently ,
2709+ wantContains : "Moved Permanently" ,
2710+ },
26382711 {
26392712 name : "adding auth header" ,
26402713 config : config.UIMetricsProxy {
@@ -2671,6 +2744,40 @@ func TestUIEndpoint_MetricsProxy(t *testing.T) {
26712744 wantCode : http .StatusOK ,
26722745 wantContains : "RawQuery: foo=bar&encoded=test%5B0%5D%26%26test%5B1%5D%3D%3D%21%40%C2%A3%24%25%5E" ,
26732746 },
2747+ {
2748+ name : "targetURL exactly matches BaseURL without trailing slash" ,
2749+ config : config.UIMetricsProxy {
2750+ BaseURL : strings .TrimSuffix (backendURL , "/some/prefix" ),
2751+ },
2752+ path : endpointPath + "/" ,
2753+ wantCode : http .StatusNotFound ,
2754+ wantContains : "not found on backend" ,
2755+ },
2756+ {
2757+ name : "targetURL matches BaseURL prefix with trailing slash" ,
2758+ config : config.UIMetricsProxy {
2759+ BaseURL : backendURL ,
2760+ },
2761+ path : endpointPath + "/ok" ,
2762+ wantCode : http .StatusOK ,
2763+ wantContains : "OK" ,
2764+ },
2765+ {
2766+ name : "targetURL fails prefix check - different domain" ,
2767+ config : config.UIMetricsProxy {
2768+ BaseURL : "http://localhost:9999/metrics" ,
2769+ },
2770+ path : endpointPath + "/../../evil.com/attack" ,
2771+ wantCode : http .StatusMovedPermanently ,
2772+ },
2773+ {
2774+ name : "targetURL fails prefix check - sibling path escape" ,
2775+ config : config.UIMetricsProxy {
2776+ BaseURL : backend .URL + "/secure/metrics" ,
2777+ },
2778+ path : endpointPath + "/../../../public/data" ,
2779+ wantCode : http .StatusMovedPermanently ,
2780+ },
26742781 }
26752782
26762783 for _ , tc := range cases {
@@ -2715,3 +2822,127 @@ func TestUIEndpoint_MetricsProxy(t *testing.T) {
27152822 })
27162823 }
27172824}
2825+
2826+ func TestUIEndpoint_MetricsProxy_TargetURLValidation (t * testing.T ) {
2827+ if testing .Short () {
2828+ t .Skip ("too slow for testing.Short" )
2829+ }
2830+
2831+ t .Parallel ()
2832+
2833+ // Create a test backend server
2834+ backend := httptest .NewServer (http .HandlerFunc (func (w http.ResponseWriter , r * http.Request ) {
2835+ w .WriteHeader (http .StatusOK )
2836+ w .Write ([]byte ("OK" ))
2837+ }))
2838+ defer backend .Close ()
2839+
2840+ a := NewTestAgent (t , `
2841+ ui_config {
2842+ enabled = true
2843+ }
2844+ ` )
2845+ defer a .Shutdown ()
2846+
2847+ endpointPath := "/v1/internal/ui/metrics-proxy"
2848+
2849+ cases := []struct {
2850+ name string
2851+ baseURL string
2852+ requestPath string
2853+ wantCode int
2854+ wantContains string
2855+ }{
2856+ {
2857+ name : "exact BaseURL match - allowed" ,
2858+ baseURL : backend .URL + "/metrics" ,
2859+ requestPath : endpointPath + "/" ,
2860+ wantCode : http .StatusOK ,
2861+ wantContains : "OK" ,
2862+ },
2863+ {
2864+ name : "proper prefix match with trailing slash - allowed" ,
2865+ baseURL : backend .URL + "/metrics" ,
2866+ requestPath : endpointPath + "/dashboard" ,
2867+ wantCode : http .StatusOK ,
2868+ wantContains : "OK" ,
2869+ },
2870+ {
2871+ name : "baseURL without trailing slash, targetURL with prefix - allowed" ,
2872+ baseURL : strings .TrimSuffix (backend .URL , "/" ) + "/metrics" ,
2873+ requestPath : endpointPath + "/dashboard" ,
2874+ wantCode : http .StatusOK ,
2875+ wantContains : "OK" ,
2876+ },
2877+ {
2878+ name : "baseURL with trailing slash, targetURL matches prefix - allowed" ,
2879+ baseURL : backend .URL + "/metrics/" ,
2880+ requestPath : endpointPath + "/dashboard" ,
2881+ wantCode : http .StatusOK ,
2882+ wantContains : "OK" ,
2883+ },
2884+ {
2885+ name : "targetURL escapes from baseURL prefix - blocked" ,
2886+ baseURL : backend .URL + "/secure/metrics" ,
2887+ requestPath : endpointPath + "/../../public/data" ,
2888+ wantCode : http .StatusMovedPermanently ,
2889+ wantContains : "Moved Permanently" ,
2890+ },
2891+ {
2892+ name : "targetURL attempts sibling directory access - blocked" ,
2893+ baseURL : backend .URL + "/app/metrics" ,
2894+ requestPath : endpointPath + "/../admin/config" ,
2895+ wantCode : http .StatusMovedPermanently ,
2896+ wantContains : "Moved Permanently" ,
2897+ },
2898+ {
2899+ name : "targetURL with encoded dots - path.Clean prevents traversal" ,
2900+ baseURL : backend .URL + "/app/metrics" ,
2901+ requestPath : endpointPath + "/%2e%2e/admin/config" ,
2902+ wantCode : http .StatusOK ,
2903+ wantContains : "OK" ,
2904+ },
2905+ {
2906+ name : "baseURL validation - exact match is allowed" ,
2907+ baseURL : backend .URL ,
2908+ requestPath : endpointPath + "/" ,
2909+ wantCode : http .StatusOK ,
2910+ wantContains : "OK" ,
2911+ },
2912+ {
2913+ name : "baseURL validation - proper prefix is allowed" ,
2914+ baseURL : backend .URL + "/secure" ,
2915+ requestPath : endpointPath + "/dashboard" ,
2916+ wantCode : http .StatusOK ,
2917+ wantContains : "OK" ,
2918+ },
2919+ }
2920+
2921+ for _ , tc := range cases {
2922+ tc := tc
2923+ t .Run (tc .name , func (t * testing.T ) {
2924+ // Configure the metrics proxy with the specific BaseURL
2925+ cfg := * a .config
2926+ cfg .UIConfig .MetricsProxy = config.UIMetricsProxy {
2927+ BaseURL : tc .baseURL ,
2928+ }
2929+
2930+ require .NoError (t , a .reloadConfigInternal (& cfg ))
2931+
2932+ // Make the request
2933+ h := a .srv .handler ()
2934+ req := httptest .NewRequest ("GET" , tc .requestPath , nil )
2935+ rec := httptest .NewRecorder ()
2936+
2937+ h .ServeHTTP (rec , req )
2938+
2939+ require .Equal (t , tc .wantCode , rec .Code ,
2940+ "Wrong status code for %s. Body = %s" , tc .name , rec .Body .String ())
2941+
2942+ if tc .wantContains != "" {
2943+ require .Contains (t , rec .Body .String (), tc .wantContains ,
2944+ "Response body should contain expected text for %s" , tc .name )
2945+ }
2946+ })
2947+ }
2948+ }
0 commit comments