Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 25 additions & 3 deletions server/middleware/dashboard.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ import (
"encoding/json"
"net"
"net/http"
"net/url"
"strings"
"sync"
"time"

Expand Down Expand Up @@ -242,9 +244,29 @@ func looksForwarded(r *http.Request) bool {
h.Get("Cf-Ray") != ""
}

// originTrusted reports whether a request's Origin header (if any) matches its
// own Host. A browser cannot forge Origin, so a present-but-mismatched Origin
// means the request was sent cross-site by a page running in the user's
// browser (drive-by CSRF) even though the TCP connection itself is loopback.
// Non-browser callers (curl, the enx CLI, local scripts) never send an Origin
// header at all, so their requests are unaffected.
func originTrusted(r *http.Request) bool {
origin := r.Header.Get("Origin")
if origin == "" {
return true
}
u, err := url.Parse(origin)
if err != nil {
return false
}
return strings.EqualFold(u.Host, r.Host)
}

// TrustedLocal reports whether the request genuinely originates on this machine:
// a loopback TCP peer that did NOT arrive via the tunnel/proxy. Remote requests
// (including anything through cloudflared) return false and must authenticate.
// a loopback TCP peer that did NOT arrive via the tunnel/proxy, and — if the
// request carries an Origin header at all — one that matches this request's
// own Host. Remote requests (including anything through cloudflared) and
// cross-site browser requests both return false and must authenticate.
func TrustedLocal(r *http.Request) bool {
return peerIsLoopback(r) && !looksForwarded(r)
return peerIsLoopback(r) && !looksForwarded(r) && originTrusted(r)
}
92 changes: 92 additions & 0 deletions server/middleware/dashboard_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
package middleware

import (
"net/http"
"net/http/httptest"
"testing"
)

func TestTrustedLocal(t *testing.T) {
tests := []struct {
name string
remoteAddr string
host string
origin string
forwarded bool
want bool
}{
{
name: "loopback, no origin header",
remoteAddr: "127.0.0.1:54321",
host: "127.0.0.1:1430",
want: true,
},
{
name: "loopback, origin matches host",
remoteAddr: "127.0.0.1:54321",
host: "127.0.0.1:1430",
origin: "http://127.0.0.1:1430",
want: true,
},
{
name: "loopback, origin host mismatch",
remoteAddr: "127.0.0.1:54321",
host: "127.0.0.1:1430",
origin: "http://evil.example.com",
want: false,
},
{
name: "loopback, origin port mismatch",
remoteAddr: "127.0.0.1:54321",
host: "127.0.0.1:1430",
origin: "http://127.0.0.1:9999",
want: false,
},
{
name: "loopback, origin is the literal null",
remoteAddr: "127.0.0.1:54321",
host: "127.0.0.1:1430",
origin: "null",
want: false,
},
{
name: "loopback, malformed origin",
remoteAddr: "127.0.0.1:54321",
host: "127.0.0.1:1430",
origin: "http://[::1]:notaport",
want: false,
},
{
name: "loopback, origin matches host but request is forwarded",
remoteAddr: "127.0.0.1:54321",
host: "127.0.0.1:1430",
origin: "http://127.0.0.1:1430",
forwarded: true,
want: false,
},
{
name: "non-loopback peer",
remoteAddr: "203.0.113.7:54321",
host: "127.0.0.1:1430",
want: false,
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
r := httptest.NewRequest(http.MethodPost, "http://"+tt.host+"/api/agent/exec", nil)
r.RemoteAddr = tt.remoteAddr
r.Host = tt.host
if tt.origin != "" {
r.Header.Set("Origin", tt.origin)
}
if tt.forwarded {
r.Header.Set("X-Forwarded-For", "198.51.100.9")
}

if got := TrustedLocal(r); got != tt.want {
t.Errorf("TrustedLocal() = %v, want %v", got, tt.want)
}
})
}
}