Add the HTTP hardening the policy requires before 1.0 (closes #108)
check / check (push) Canceled after 0s
check / check (push) Canceled after 0s
Every answer carries the six security headers REPO_POLICIES.md lists. The Content-Security-Policy allows inline content only with a nonce made for that answer; the pages put it on their style and script blocks and drop their four style attributes. The request log and /ip/ take a forwarded address only from a proxy listed in TRUSTED_PROXIES, which stops the start when set but invalid: the last X-Forwarded-For entry that is not a trusted proxy. X-Real-IP and True-Client-IP are no longer read. The JSON routes and the health check send fixed messages in place of database, parser and resolver errors, and log the detail. Model: opus-5-5
This commit was merged in pull request #132.
This commit is contained in:
@@ -28,6 +28,14 @@ const (
|
||||
testPeerV6 = "2001:db8::1"
|
||||
)
|
||||
|
||||
// Paths several tests request.
|
||||
const (
|
||||
asPagePath = "/as/64500"
|
||||
prefixLengthPagePath = "/prefixlength/24"
|
||||
statusJSONPath = "/status.json"
|
||||
statsPath = "/api/v1/stats"
|
||||
)
|
||||
|
||||
// The names the stats handler tests report the two stats handlers by.
|
||||
const (
|
||||
statusJSONHandlerName = "status.json"
|
||||
@@ -429,7 +437,7 @@ func TestPagesSayOriginZeroIsNoKnownOrigin(t *testing.T) {
|
||||
|
||||
s := New(db, streamer.New(logger.New(), metrics.New()), logger.New(), &config.Config{})
|
||||
|
||||
for _, path := range []string{"/prefix/" + testPrefixV4, "/prefixlength/24"} {
|
||||
for _, path := range []string{"/prefix/" + testPrefixV4, prefixLengthPagePath} {
|
||||
rec := httptest.NewRecorder()
|
||||
s.router.ServeHTTP(rec, httptest.NewRequestWithContext(
|
||||
t.Context(), http.MethodGet, path, nil))
|
||||
@@ -610,13 +618,13 @@ func TestRoutesAnswer504WhenDatabaseReadIsCutOff(t *testing.T) {
|
||||
path string
|
||||
wantJSONBody bool
|
||||
}{
|
||||
{path: "/as/64500"},
|
||||
{path: asPagePath},
|
||||
{path: "/prefix/192.0.2.0/24"},
|
||||
{path: "/prefixlength/24"},
|
||||
{path: prefixLengthPagePath},
|
||||
{path: "/prefixlength6/48"},
|
||||
{path: "/status.json", wantJSONBody: true},
|
||||
{path: statusJSONPath, wantJSONBody: true},
|
||||
{path: "/ip/192.0.2.1", wantJSONBody: true},
|
||||
{path: "/api/v1/stats", wantJSONBody: true},
|
||||
{path: statsPath, wantJSONBody: true},
|
||||
{path: "/api/v1/ip/192.0.2.1", wantJSONBody: true},
|
||||
{path: "/api/v1/as/64500", wantJSONBody: true},
|
||||
{path: "/api/v1/prefix/192.0.2.0/24", wantJSONBody: true},
|
||||
@@ -689,9 +697,9 @@ func TestRoutesAnswer504WhenSecondDatabaseReadIsCutOff(t *testing.T) {
|
||||
path string
|
||||
wantJSONBody bool
|
||||
}{
|
||||
{path: "/as/64500"},
|
||||
{path: "/status.json", wantJSONBody: true},
|
||||
{path: "/api/v1/stats", wantJSONBody: true},
|
||||
{path: asPagePath},
|
||||
{path: statusJSONPath, wantJSONBody: true},
|
||||
{path: statsPath, wantJSONBody: true},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
@@ -758,6 +766,86 @@ func checkJSONErrorCode(
|
||||
}
|
||||
}
|
||||
|
||||
// errDiskIO is the database driver's error failingDB answers with.
|
||||
var errDiskIO = errors.New("disk I/O error")
|
||||
|
||||
// failingDB embeds database.Store (left nil) and fails the reads of the JSON
|
||||
// routes and the health check's ping with errDiskIO. Every other Store method
|
||||
// is unused and would panic if called.
|
||||
type failingDB struct {
|
||||
database.Store
|
||||
}
|
||||
|
||||
func (failingDB) Ping(_ context.Context) error {
|
||||
return errDiskIO
|
||||
}
|
||||
|
||||
func (failingDB) GetStatsContext(_ context.Context) (database.Stats, error) {
|
||||
return database.Stats{}, errDiskIO
|
||||
}
|
||||
|
||||
func (failingDB) GetIPInfoContext(
|
||||
_ context.Context, _ string,
|
||||
) (*database.IPInfo, error) {
|
||||
return nil, errDiskIO
|
||||
}
|
||||
|
||||
func (failingDB) GetASDetailsContext(
|
||||
_ context.Context, _ int,
|
||||
) (*database.ASN, []database.LiveRoute, error) {
|
||||
return nil, nil, errDiskIO
|
||||
}
|
||||
|
||||
func (failingDB) GetPrefixDetailsContext(
|
||||
_ context.Context, _ string,
|
||||
) ([]database.LiveRoute, map[int]database.ASN, error) {
|
||||
return nil, nil, errDiskIO
|
||||
}
|
||||
|
||||
// TestJSONRoutesDoNotSendErrorDetails sends a request to each JSON route and
|
||||
// the health check with a store whose reads fail with the driver's error text,
|
||||
// and a prefix that does not parse, and checks that no answer carries the
|
||||
// driver's or the prefix parser's text. Before the fix the answers sent it to
|
||||
// the client (https://git.eeqj.de/sneak/routewatch/issues/108). The IP lookup
|
||||
// answers 404, as it does whenever no address could be looked up.
|
||||
func TestJSONRoutesDoNotSendErrorDetails(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
s := New(failingDB{}, streamer.New(logger.New(), metrics.New()), logger.New(),
|
||||
&config.Config{})
|
||||
|
||||
tests := []struct {
|
||||
path string
|
||||
wantCode int
|
||||
}{
|
||||
{"/.well-known/healthcheck.json", http.StatusServiceUnavailable},
|
||||
{statusJSONPath, http.StatusInternalServerError},
|
||||
{statsPath, http.StatusInternalServerError},
|
||||
{"/api/v1/as/64500", http.StatusInternalServerError},
|
||||
{"/api/v1/prefix/192.0.2.0/24", http.StatusInternalServerError},
|
||||
{"/api/v1/ip/192.0.2.1", http.StatusNotFound},
|
||||
{"/api/v1/prefix/foo/24", http.StatusBadRequest},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
rec := httptest.NewRecorder()
|
||||
s.router.ServeHTTP(rec, httptest.NewRequestWithContext(
|
||||
t.Context(), http.MethodGet, tt.path, nil))
|
||||
|
||||
if rec.Code != tt.wantCode {
|
||||
t.Errorf("%s: status %d, want %d; body %s",
|
||||
tt.path, rec.Code, tt.wantCode, rec.Body)
|
||||
}
|
||||
|
||||
for _, detail := range []string{errDiskIO.Error(), "netip"} {
|
||||
if strings.Contains(rec.Body.String(), detail) {
|
||||
t.Errorf("%s: body %s carries the error text %q",
|
||||
tt.path, rec.Body, detail)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// settledGoroutineCount lets transient goroutines finish, then reports the
|
||||
// current count.
|
||||
func settledGoroutineCount() int {
|
||||
|
||||
Reference in New Issue
Block a user