mirror of
https://github.com/tailscale/tailscale.git
synced 2025-05-21 09:48:31 +00:00
cmd/proxy-to-grafana: strip X-Webauth* headers from all requests (#15985)
Update proxy-to-grafana to strip any X-Webauth prefixed headers passed by the client in *every* request, not just those to /login. /api/ routes will also accept these headers to authenticate users, necessitating their removal to prevent forgery. Updates tailscale/corp#28687 Signed-off-by: Patrick O'Doherty <patrick@tailscale.com>
This commit is contained in:
parent
824985afe1
commit
336b3b7df0
@ -53,7 +53,7 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"tailscale.com/client/local"
|
"tailscale.com/client/tailscale/apitype"
|
||||||
"tailscale.com/tailcfg"
|
"tailscale.com/tailcfg"
|
||||||
"tailscale.com/tsnet"
|
"tailscale.com/tsnet"
|
||||||
)
|
)
|
||||||
@ -195,13 +195,7 @@ func main() {
|
|||||||
log.Fatal(http.Serve(ln, proxy))
|
log.Fatal(http.Serve(ln, proxy))
|
||||||
}
|
}
|
||||||
|
|
||||||
func modifyRequest(req *http.Request, localClient *local.Client) {
|
func modifyRequest(req *http.Request, localClient whoisIdentitySource) {
|
||||||
// with enable_login_token set to true, we get a cookie that handles
|
|
||||||
// auth for paths that are not /login
|
|
||||||
if req.URL.Path != "/login" {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Delete any existing X-Webauth-* headers to prevent possible spoofing
|
// Delete any existing X-Webauth-* headers to prevent possible spoofing
|
||||||
// if getting Tailnet identity fails.
|
// if getting Tailnet identity fails.
|
||||||
for h := range req.Header {
|
for h := range req.Header {
|
||||||
@ -210,6 +204,13 @@ func modifyRequest(req *http.Request, localClient *local.Client) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Set the X-Webauth-* headers only for the /login path
|
||||||
|
// With enable_login_token set to true, we get a cookie that handles
|
||||||
|
// auth for paths that are not /login
|
||||||
|
if req.URL.Path != "/login" {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
user, role, err := getTailscaleIdentity(req.Context(), localClient, req.RemoteAddr)
|
user, role, err := getTailscaleIdentity(req.Context(), localClient, req.RemoteAddr)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("error getting Tailscale user: %v", err)
|
log.Printf("error getting Tailscale user: %v", err)
|
||||||
@ -221,7 +222,7 @@ func modifyRequest(req *http.Request, localClient *local.Client) {
|
|||||||
req.Header.Set("X-Webauth-Role", role.String())
|
req.Header.Set("X-Webauth-Role", role.String())
|
||||||
}
|
}
|
||||||
|
|
||||||
func getTailscaleIdentity(ctx context.Context, localClient *local.Client, ipPort string) (*tailcfg.UserProfile, grafanaRole, error) {
|
func getTailscaleIdentity(ctx context.Context, localClient whoisIdentitySource, ipPort string) (*tailcfg.UserProfile, grafanaRole, error) {
|
||||||
whois, err := localClient.WhoIs(ctx, ipPort)
|
whois, err := localClient.WhoIs(ctx, ipPort)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, ViewerRole, fmt.Errorf("failed to identify remote host: %w", err)
|
return nil, ViewerRole, fmt.Errorf("failed to identify remote host: %w", err)
|
||||||
@ -248,3 +249,7 @@ func getTailscaleIdentity(ctx context.Context, localClient *local.Client, ipPort
|
|||||||
|
|
||||||
return whois.UserProfile, role, nil
|
return whois.UserProfile, role, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type whoisIdentitySource interface {
|
||||||
|
WhoIs(ctx context.Context, ipPort string) (*apitype.WhoIsResponse, error)
|
||||||
|
}
|
||||||
|
77
cmd/proxy-to-grafana/proxy-to-grafana_test.go
Normal file
77
cmd/proxy-to-grafana/proxy-to-grafana_test.go
Normal file
@ -0,0 +1,77 @@
|
|||||||
|
// Copyright (c) Tailscale Inc & AUTHORS
|
||||||
|
// SPDX-License-Identifier: BSD-3-Clause
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"net/http/httptest"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"tailscale.com/client/tailscale/apitype"
|
||||||
|
"tailscale.com/tailcfg"
|
||||||
|
)
|
||||||
|
|
||||||
|
type mockWhoisSource struct {
|
||||||
|
id *apitype.WhoIsResponse
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *mockWhoisSource) WhoIs(ctx context.Context, remoteAddr string) (*apitype.WhoIsResponse, error) {
|
||||||
|
if m.id == nil {
|
||||||
|
return nil, fmt.Errorf("missing mock identity")
|
||||||
|
}
|
||||||
|
return m.id, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var whois = &apitype.WhoIsResponse{
|
||||||
|
UserProfile: &tailcfg.UserProfile{
|
||||||
|
LoginName: "foobar@example.com",
|
||||||
|
DisplayName: "Foobar",
|
||||||
|
},
|
||||||
|
Node: &tailcfg.Node{
|
||||||
|
ID: 1,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestModifyRequest_Login(t *testing.T) {
|
||||||
|
req := httptest.NewRequest("GET", "/login", nil)
|
||||||
|
modifyRequest(req, &mockWhoisSource{id: whois})
|
||||||
|
|
||||||
|
if got := req.Header.Get("X-Webauth-User"); got != "foobar@example.com" {
|
||||||
|
t.Errorf("X-Webauth-User = %q; want %q", got, "foobar@example.com")
|
||||||
|
}
|
||||||
|
|
||||||
|
if got := req.Header.Get("X-Webauth-Role"); got != "Viewer" {
|
||||||
|
t.Errorf("X-Webauth-Role = %q; want %q", got, "Viewer")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestModifyRequest_RemoveHeaders_Login(t *testing.T) {
|
||||||
|
req := httptest.NewRequest("GET", "/login", nil)
|
||||||
|
req.Header.Set("X-Webauth-User", "malicious@example.com")
|
||||||
|
req.Header.Set("X-Webauth-Role", "Admin")
|
||||||
|
|
||||||
|
modifyRequest(req, &mockWhoisSource{id: whois})
|
||||||
|
|
||||||
|
if got := req.Header.Get("X-Webauth-User"); got != "foobar@example.com" {
|
||||||
|
t.Errorf("X-Webauth-User = %q; want %q", got, "foobar@example.com")
|
||||||
|
}
|
||||||
|
if got := req.Header.Get("X-Webauth-Role"); got != "Viewer" {
|
||||||
|
t.Errorf("X-Webauth-Role = %q; want %q", got, "Viewer")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestModifyRequest_RemoveHeaders_API(t *testing.T) {
|
||||||
|
req := httptest.NewRequest("DELETE", "/api/org/users/1", nil)
|
||||||
|
req.Header.Set("X-Webauth-User", "malicious@example.com")
|
||||||
|
req.Header.Set("X-Webauth-Role", "Admin")
|
||||||
|
|
||||||
|
modifyRequest(req, &mockWhoisSource{id: whois})
|
||||||
|
|
||||||
|
if got := req.Header.Get("X-Webauth-User"); got != "" {
|
||||||
|
t.Errorf("X-Webauth-User = %q; want %q", got, "")
|
||||||
|
}
|
||||||
|
if got := req.Header.Get("X-Webauth-Role"); got != "" {
|
||||||
|
t.Errorf("X-Webauth-Role = %q; want %q", got, "")
|
||||||
|
}
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user