fix(config): refuse plaintext auth-source urls to public hosts
An auth_source url could be http:// to any host. Anyone on the path to a public root, or anyone who can spoof its DNS name, can then answer hasJoined with a 200 and log in as any player of that source, including a third-party account linked to staff. The player's IP also travels in cleartext. Mojang logins are unaffected, since that source is built in over https. Config load now refuses http:// unless the host is localhost or a loopback or private IP address (127.0.0.0/8, ::1, 10/8, 172.16/12, 192.168/16, fc00::/7), so a root on the same host or the LAN still works without TLS. The decision is made on the literal host because nothing is resolved at load time, so a LAN root named by hostname needs its IP address or https. The error says what to change. The new test covers public names and addresses, link-local, 0.0.0.0 and the first address past 172.16/12 (all refused over http, all accepted over https), and the loopback and private forms that stay allowed. It fails on the old check.
This commit is contained in:
2 files changed
+37
No files matched your search
@@ -6,6 +6,7 @@ package config
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
"net/url"
|
||||
"regexp"
|
||||
"strings"
|
||||
@@ -414,6 +415,18 @@ func hasJoinedURLProblem(u string) string {
|
||||
return "has no host"
|
||||
case strings.ContainsAny(u, "?#"):
|
||||
return "must not carry a query or fragment; the username and serverId parameters are appended to it"
|
||||
case p.Scheme == "http" && !plaintextHostOK(p.Hostname()):
|
||||
return "sends logins in plaintext to a public host, where anyone on the path can answer as any player of this source; use https://, or http:// only for localhost or a loopback or private IP address"
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// plaintextHostOK is decided on the literal host because nothing is resolved at load time,
|
||||
// so a LAN root named by hostname needs its IP address or https.
|
||||
func plaintextHostOK(host string) bool {
|
||||
if strings.EqualFold(host, "localhost") {
|
||||
return true
|
||||
}
|
||||
ip := net.ParseIP(host)
|
||||
return ip != nil && (ip.IsLoopback() || ip.IsPrivate())
|
||||
}
|
||||
@@ -385,6 +385,30 @@ func TestLoadRejectsUnqueryableAuthSourceURL(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// A source reached over plaintext can be answered by anyone on the path, who can then log in
|
||||
// as any player of that source. Only a same-host or private-network root may skip TLS, and
|
||||
// that is decided on the literal host, since nothing is resolved at load time.
|
||||
func TestLoadRejectsPlaintextPublicAuthSource(t *testing.T) {
|
||||
load := func(u string) error {
|
||||
_, err := config.LoadNano(writeTOML(t, "[[auth_source]]\ntag = \"a\"\nprefix = \"AA\"\nurl = \""+u+"\"\n"))
|
||||
return err
|
||||
}
|
||||
for _, host := range []string{"ygg.example.net", "203.0.113.9", "ygg.lan", "172.32.0.1", "169.254.1.1", "0.0.0.0", "[2001:db8::1]"} {
|
||||
u := "http://" + host + "/hasJoined"
|
||||
if err := load(u); err == nil || !strings.Contains(err.Error(), "https://") {
|
||||
t.Errorf("url %q: err = %v, want a refusal that asks for https://", u, err)
|
||||
}
|
||||
if err := load("https://" + host + "/hasJoined"); err != nil {
|
||||
t.Errorf("the same host over https must load: %v", err)
|
||||
}
|
||||
}
|
||||
for _, host := range []string{"localhost", "LOCALHOST:8080", "127.0.0.1:8080", "127.1.2.3", "[::1]:8080", "10.0.0.5", "172.16.3.4", "192.168.1.2", "[fd00::1]"} {
|
||||
if err := load("http://" + host + "/hasJoined"); err != nil {
|
||||
t.Errorf("a plaintext root on %s must load: %v", host, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestLoadNanoAcceptsMinimalConfig is the linchpin of the Felis-nano fold: a nano host has no
|
||||
// Postgres and no FQDN, so LoadNano must accept a felis.toml carrying ONLY [[auth_source]] —
|
||||
// the control-plane requirements (database.url, root_domain) that full Load enforces are
|
||||
|
||||
Reference in new issue
Block a user