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 is contained in:
@@ -758,6 +758,85 @@ 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 that
|
||||
// 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
|
||||
detail string // the error text the answer must not carry
|
||||
}{
|
||||
{"/.well-known/healthcheck.json", http.StatusServiceUnavailable, "disk I/O"},
|
||||
{"/status.json", http.StatusInternalServerError, "disk I/O"},
|
||||
{"/api/v1/stats", http.StatusInternalServerError, "disk I/O"},
|
||||
{"/api/v1/as/64500", http.StatusInternalServerError, "disk I/O"},
|
||||
{"/api/v1/prefix/192.0.2.0/24", http.StatusInternalServerError, "disk I/O"},
|
||||
{"/api/v1/ip/192.0.2.1", http.StatusNotFound, "disk I/O"},
|
||||
{"/api/v1/prefix/foo/24", http.StatusBadRequest, "netip"},
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
if strings.Contains(rec.Body.String(), tt.detail) {
|
||||
t.Errorf("%s: body %s carries the error text %q",
|
||||
tt.path, rec.Body, tt.detail)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// settledGoroutineCount lets transient goroutines finish, then reports the
|
||||
// current count.
|
||||
func settledGoroutineCount() int {
|
||||
|
||||
Reference in New Issue
Block a user