Skip to content

Commit 9d0e8d8

Browse files
committed
Refuse to send credentials to a remote host over plain HTTP
The simulator posts a password to /api/v1/auth/login and then puts the returned token on every report through bearerTransport. Against an http:// destination both go on the wire in cleartext. Reject plain HTTP before login, except for loopback: the default is http://localhost:8080 and that is how the simulator is normally run, so a blanket HTTPS requirement would break the tool for its actual use.
1 parent 5d98d4d commit 9d0e8d8

2 files changed

Lines changed: 69 additions & 0 deletions

File tree

cmd/simulator/main.go

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,9 @@ import (
99
"fmt"
1010
"io"
1111
"log"
12+
"net"
1213
"net/http"
14+
"net/url"
1315
"os"
1416
"os/signal"
1517
"sync"
@@ -34,6 +36,41 @@ type stats struct {
3436
totalMS atomic.Int64
3537
}
3638

39+
// checkBaseURL rejects a destination that would put the password and the
40+
// session token on the wire in cleartext. Plain HTTP stays allowed for
41+
// loopback, which is the default and the only way the simulator is normally
42+
// run, but anything remote has to be HTTPS.
43+
func checkBaseURL(raw string) error {
44+
u, err := url.Parse(raw)
45+
if err != nil {
46+
return fmt.Errorf("invalid -url %q: %w", raw, err)
47+
}
48+
if u.Host == "" {
49+
return fmt.Errorf("invalid -url %q: no host", raw)
50+
}
51+
switch u.Scheme {
52+
case "https":
53+
return nil
54+
case "http":
55+
if isLoopbackHost(u.Hostname()) {
56+
return nil
57+
}
58+
return fmt.Errorf("-url %q sends the login password and the bearer token in cleartext; use https for a remote host", raw)
59+
default:
60+
return fmt.Errorf("invalid -url %q: scheme must be http or https", raw)
61+
}
62+
}
63+
64+
func isLoopbackHost(host string) bool {
65+
if host == "localhost" {
66+
return true
67+
}
68+
if ip := net.ParseIP(host); ip != nil {
69+
return ip.IsLoopback()
70+
}
71+
return false
72+
}
73+
3774
func main() {
3875
baseURL := flag.String("url", "http://localhost:8080", "Server base URL")
3976
numVehicles := flag.Int("vehicles", 10, "Number of simulated vehicles")
@@ -58,6 +95,10 @@ func main() {
5895
defer cancel()
5996
}
6097

98+
if err := checkBaseURL(*baseURL); err != nil {
99+
log.Fatal(err)
100+
}
101+
61102
if *email == "" || *password == "" {
62103
log.Fatal("email and password are required: POST /api/v1/locations is authenticated, " +
63104
"so pass -email/-password or set ADMIN_BOOTSTRAP_EMAIL and ADMIN_BOOTSTRAP_PASSWORD")

cmd/simulator/main_test.go

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -358,3 +358,31 @@ func TestBearerTransportSetsAuthorizationHeader(t *testing.T) {
358358

359359
assert.Equal(t, "Bearer tok-123", gotAuth)
360360
}
361+
362+
func TestCheckBaseURL(t *testing.T) {
363+
allowed := []string{
364+
"http://localhost:8080",
365+
"http://127.0.0.1:8080",
366+
"http://[::1]:8080",
367+
"https://example.org",
368+
"https://example.org:8443/base",
369+
}
370+
for _, raw := range allowed {
371+
if err := checkBaseURL(raw); err != nil {
372+
t.Errorf("checkBaseURL(%q) = %v, want nil", raw, err)
373+
}
374+
}
375+
376+
rejected := []string{
377+
"http://example.org", // credentials in cleartext to a remote host
378+
"http://192.168.1.10:8080", // private, still not loopback
379+
"ftp://example.org", // not an HTTP scheme
380+
"https://", // no host
381+
"://nonsense", // unparseable
382+
}
383+
for _, raw := range rejected {
384+
if err := checkBaseURL(raw); err == nil {
385+
t.Errorf("checkBaseURL(%q) = nil, want an error", raw)
386+
}
387+
}
388+
}

0 commit comments

Comments
 (0)