mirror of
https://github.com/netbirdio/netbird.git
synced 2026-10-12 08:29:06 +02:00
DebugBundle uploads when either the upload flag or the upload URL is set, but the privilege gate was told only about the flag. With an MDM policy pinning the destination the gate blanks the caller's URL, so a request carrying a URL and no flag reached the empty-URL branch and found upload false: nothing to weaken, let it through. The daemon then uploaded as root with TLS verification disabled on behalf of an unprivileged caller. A client older than the upload flag sends exactly that shape, a URL and nothing else, so this needs no crafted request to reach. The gate now derives the upload intent from the URL rather than trusting the caller's flag, and DebugBundle computes it once so the gate and the local-only early return cannot drift apart.
127 lines
5.2 KiB
Go
127 lines
5.2 KiB
Go
//go:build !android && !ios
|
|
|
|
package server
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"net/url"
|
|
"os"
|
|
"strings"
|
|
|
|
"google.golang.org/grpc/codes"
|
|
gstatus "google.golang.org/grpc/status"
|
|
|
|
"github.com/netbirdio/netbird/client/configs"
|
|
"github.com/netbirdio/netbird/client/internal/debug"
|
|
"github.com/netbirdio/netbird/client/internal/ipcauth"
|
|
"github.com/netbirdio/netbird/upload-server/types"
|
|
)
|
|
|
|
// uiLogFileName is the only file name the daemon accepts as a UI log path. The
|
|
// UI (writer), this validation, and the bundle collector all read it from
|
|
// configs so they cannot drift.
|
|
const uiLogFileName = configs.UILogFile
|
|
|
|
// uiLogOpener opens the registered UI log, and its rotated siblings, on behalf
|
|
// of the caller requesting the bundle: OpenOwnedFile then collects the log only
|
|
// when that caller owns it (or is privileged). identified is false on a socket
|
|
// that carries no caller identity, in which case nothing is opened.
|
|
func uiLogOpener(id ipcauth.Identity, identified bool) debug.LogOpener {
|
|
return func(path string) (*os.File, error) {
|
|
if !identified {
|
|
return nil, fmt.Errorf("bundle requester has no verified identity")
|
|
}
|
|
return ipcauth.OpenOwnedFile(id, path)
|
|
}
|
|
}
|
|
|
|
// requirePrivilegeForUploadURL restricts where the daemon may send a debug
|
|
// bundle. The bundle holds the daemon's own logs and state, and the daemon
|
|
// fetches the upload URL itself, so an unrestricted endpoint turns the daemon
|
|
// into both an exfiltration channel and a request forwarder that reaches
|
|
// services only it can talk to.
|
|
//
|
|
// The upload service NetBird publishes is open to any caller, since that is what
|
|
// the CLI and the desktop UI use. Any other endpoint, self-hosted upload servers
|
|
// included, requires a privileged caller. Plaintext is refused for everyone: the
|
|
// daemon fetches the URL and then PUTs the bundle to whatever that fetch returns,
|
|
// so an http hop is a place to intercept the bundle or the redirect.
|
|
//
|
|
// insecure relaxes transport security (http, or an untrusted TLS certificate)
|
|
// for a self-hosted server. It weakens a root-privileged upload, so it is
|
|
// refused for an unprivileged caller regardless of the host. upload says whether
|
|
// the request asks for an upload at all; without one there is nothing to weaken.
|
|
//
|
|
// mdmPinned says an MDM policy already fixes the destination. The caller's URL
|
|
// is then discarded before the upload, so gating on it would only turn a bundle
|
|
// that was going to the pinned host anyway into a refusal. Transport security
|
|
// still is gated: relaxing TLS towards the pinned host is a real weakening.
|
|
func requirePrivilegeForUploadURL(ctx context.Context, rawURL string, insecure, upload, mdmPinned bool) error {
|
|
// A named URL is itself a request to upload, since DebugBundle uploads when
|
|
// either the flag or the URL is set and a client older than the flag sends
|
|
// only the URL. Derived here rather than taken on trust from the caller:
|
|
// blanking rawURL below would otherwise turn an absent flag into "no upload
|
|
// to weaken" and let an unprivileged caller relax TLS on a root-run upload.
|
|
upload = upload || rawURL != ""
|
|
|
|
if mdmPinned {
|
|
rawURL = ""
|
|
}
|
|
|
|
if rawURL == "" {
|
|
// An empty URL with upload requested is not "no upload": the daemon then
|
|
// resolves the destination the management server published. Relaxing TLS
|
|
// on the way there exposes the bundle exactly as naming the host outright
|
|
// would, so it needs the same privilege. Without an upload there is no
|
|
// destination to weaken and a local-only bundle must not be refused.
|
|
if insecure && upload {
|
|
return denyPrivileged(ctx,
|
|
"uploading a debug bundle without transport security (--upload-bundle-insecure)",
|
|
ipcauth.ElevatedCommand("netbird debug bundle -U --upload-bundle-insecure"))
|
|
}
|
|
return nil
|
|
}
|
|
|
|
parsed, err := url.Parse(rawURL)
|
|
if err != nil {
|
|
return gstatus.Errorf(codes.InvalidArgument, "parse upload URL: %v", err)
|
|
}
|
|
|
|
// --insecure relaxes https to http or an untrusted certificate; it does not
|
|
// widen the URL to arbitrary schemes, so a host and http/https are required
|
|
// before the insecure branch takes over.
|
|
if parsed.Host == "" || (parsed.Scheme != "https" && parsed.Scheme != "http") {
|
|
return gstatus.Errorf(codes.InvalidArgument, "upload URL must be http or https with a host")
|
|
}
|
|
|
|
if insecure {
|
|
return denyPrivileged(ctx,
|
|
"uploading a debug bundle without transport security (--upload-bundle-insecure)",
|
|
ipcauth.ElevatedCommand("netbird debug bundle -U --upload-bundle-insecure --upload-bundle-url <url>"))
|
|
}
|
|
|
|
if parsed.Scheme != "https" {
|
|
return gstatus.Errorf(codes.InvalidArgument, "upload URL must use https, got scheme %q", parsed.Scheme)
|
|
}
|
|
|
|
if isDefaultUploadService(parsed) {
|
|
return nil
|
|
}
|
|
|
|
return denyPrivileged(ctx,
|
|
"uploading a debug bundle to an upload service other than the default one",
|
|
ipcauth.ElevatedCommand("netbird debug bundle -U --upload-bundle-url <url>"))
|
|
}
|
|
|
|
// isDefaultUploadService reports whether the URL points at the upload service
|
|
// NetBird runs. Only the host is compared: the service's path may differ between
|
|
// releases, and the host is what decides who receives the bundle.
|
|
func isDefaultUploadService(parsed *url.URL) bool {
|
|
defaultURL, err := url.Parse(types.DefaultBundleURL)
|
|
if err != nil {
|
|
return false
|
|
}
|
|
return parsed.Scheme == defaultURL.Scheme && strings.EqualFold(parsed.Host, defaultURL.Host)
|
|
}
|