blob: 1e65fd365329bf27a77b06399c0b7f0689a3d69e [file]
package frontend
import (
"bytes"
"io"
"net/http"
"net/http/httptest"
"net/url"
"testing"
"github.com/stretchr/testify/require"
)
type roundTripFunc func(req *http.Request) *http.Response
func (f roundTripFunc) RoundTrip(req *http.Request) (*http.Response, error) {
return f(req), nil
}
func TestIsAllowedProxyURL(t *testing.T) {
testCases := []struct {
urlStr string
allowed bool
}{
// Allowed cases
{"https://chromium.googlesource.com/v8/v8/+log/master?format=JSON", true},
{"https://skia.googlesource.com/buildbot/+log/main?format=JSON", true},
{"https://googlesource.com/repo/+log/main", true},
{"https://a.b.googlesource.com/repo", true},
{"https://chromium.googlesource.com:443/v8/v8", true},
// Disallowed schemes
{"http://chromium.googlesource.com/v8/v8", false},
{"ftp://chromium.googlesource.com/v8/v8", false},
{"file:///etc/passwd", false},
{"javascript:alert(1)", false},
{"gopher://chromium.googlesource.com/", false},
// Disallowed domains / hosts
{"https://attacker.com/v8/v8", false},
{"https://evilgooglesource.com/v8/v8", false},
{"https://googlesource.com.attacker.com/v8/v8", false},
{"https://169.254.169.254/computeMetadata/v1/", false},
{"https://127.0.0.1:8080/", false},
{"https://localhost/", false},
// Disallowed user credentials or non-standard ports
{"https://user:pass@chromium.googlesource.com/", false},
{"https://chromium.googlesource.com:8443/v8/v8", false},
{"https://chromium.googlesource.com:80/v8/v8", false},
}
for _, tc := range testCases {
u, err := url.Parse(tc.urlStr)
if err != nil {
require.False(t, tc.allowed, "Malformed URL should not be allowed: %s", tc.urlStr)
continue
}
require.Equal(t, tc.allowed, isAllowedProxyURL(u), "URL: %s", tc.urlStr)
}
require.False(t, isAllowedProxyURL(nil))
}
func TestProxy_GetHandler_MissingURL_ReturnsBadRequest(t *testing.T) {
r := httptest.NewRequest(http.MethodGet, "/_/json/", nil)
w := httptest.NewRecorder()
Proxy_GetHandler(w, r)
require.Equal(t, http.StatusBadRequest, w.Result().StatusCode)
require.Contains(t, w.Body.String(), "Missing 'url' query parameter.")
}
func TestProxy_GetHandler_DisallowedURL_ReturnsBadRequest(t *testing.T) {
disallowedURLs := []string{
"http://chromium.googlesource.com/v8/v8",
"https://attacker.com/steal-cookie",
"https://169.254.169.254/computeMetadata/v1/",
"https://evilgooglesource.com/v8",
"https://user:pass@chromium.googlesource.com/",
"https://chromium.googlesource.com:8080/",
}
for _, u := range disallowedURLs {
r := httptest.NewRequest(http.MethodGet, "/_/json/?url="+url.QueryEscape(u), nil)
w := httptest.NewRecorder()
Proxy_GetHandler(w, r)
require.Equal(t, http.StatusBadRequest, w.Result().StatusCode, "Expected Bad Request for URL: %s", u)
}
}
func TestProxy_GetHandler_AllowedURL_ProxiesWithoutLeakingCredentials(t *testing.T) {
targetURL := "https://chromium.googlesource.com/v8/v8/+log/1234..5678?format=JSON"
expectedBody := ")]}'\n{\"log\": [{\"commit\": \"1234\"}]}"
var capturedReq *http.Request
origTransport := proxyTransport
defer func() { proxyTransport = origTransport }()
proxyTransport = roundTripFunc(func(req *http.Request) *http.Response {
capturedReq = req
return &http.Response{
StatusCode: http.StatusOK,
Header: http.Header{
"Content-Type": []string{"application/json"},
},
Body: io.NopCloser(bytes.NewBufferString(expectedBody)),
}
})
r := httptest.NewRequest(http.MethodGet, "/_/json/?url="+url.QueryEscape(targetURL), nil)
// Inject sensitive client credentials/cookies into incoming request
r.Header.Set("Cookie", "session_id=secret-session-token; skia_auth=secret-auth")
r.Header.Set("Authorization", "Bearer sensitive-oauth-token")
r.Header.Set("X-UberProxy-Signed-UpTick", "sensitive-internal-data")
r.Header.Set("X-Goog-Authenticated-User-Email", "victim@google.com")
w := httptest.NewRecorder()
Proxy_GetHandler(w, r)
// Verify the outgoing request received by the upstream proxy
require.NotNil(t, capturedReq)
require.Equal(t, targetURL, capturedReq.URL.String())
require.Empty(t, capturedReq.Header.Get("Cookie"), "Session cookies must NOT be forwarded")
require.Empty(t, capturedReq.Header.Get("Authorization"), "Authorization header must NOT be forwarded")
require.Empty(t, capturedReq.Header.Get("X-UberProxy-Signed-UpTick"), "Internal proxy headers must NOT be forwarded")
require.Empty(t, capturedReq.Header.Get("X-Goog-Authenticated-User-Email"), "Internal user identity headers must NOT be forwarded")
require.Equal(t, "application/json", capturedReq.Header.Get("Accept"))
require.Equal(t, "Skia-Perf-Proxy", capturedReq.Header.Get("User-Agent"))
// Verify the proxied response returned to the caller
res := w.Result()
require.Equal(t, http.StatusOK, res.StatusCode)
require.Equal(t, "application/json", res.Header.Get("Content-Type"))
require.Equal(t, "nosniff", res.Header.Get("X-Content-Type-Options"))
require.Equal(t, expectedBody, w.Body.String())
}