Refuse OPTIONS * like any other API request (closes #4)
check / check (push) Successful in 1m16s

net/http answered "OPTIONS *" itself, with 200, before the router, so
it skipped the credential check and the security headers. The server
now passes it to the router, which refuses it with 401 like any other
request without the credential. A test sends it to a running server,
since the handler alone never sees it.

Model: opus-5-5
This commit is contained in:
clawbot
2026-09-29 02:20:58 +00:00
parent 9ce902fb05
commit 0c5163e69e
2 changed files with 48 additions and 0 deletions
+45
View File
@@ -4,7 +4,9 @@ import (
"bytes"
"context"
"errors"
"io"
"log/slog"
"net"
"net/http"
"net/http/httptest"
"strings"
@@ -175,6 +177,49 @@ func TestNoPathIsExempt(t *testing.T) {
}
}
// TestOptionsAsterisk: "OPTIONS *" is refused like any other request.
// net/http would answer it before the handler, so this request goes to
// a running server rather than to its handler.
func TestOptionsAsterisk(t *testing.T) {
t.Parallel()
srv := newAPI("", &fakeClient{})
listener, err := (&net.ListenConfig{}).Listen(t.Context(), "tcp", "127.0.0.1:0")
if err != nil {
t.Fatal(err)
}
go func() { _ = srv.Serve(listener) }()
t.Cleanup(func() { _ = srv.Close() })
req, err := http.NewRequestWithContext(t.Context(), http.MethodOptions,
"http://"+listener.Addr().String(), nil)
if err != nil {
t.Fatal(err)
}
// The request line becomes "OPTIONS * HTTP/1.1".
req.URL.Opaque = "*"
resp, err := http.DefaultClient.Do(req)
if err != nil {
t.Fatal(err)
}
defer func() { _ = resp.Body.Close() }()
body, err := io.ReadAll(resp.Body)
if err != nil {
t.Fatal(err)
}
if resp.StatusCode != http.StatusUnauthorized || string(body) != unauthorized {
t.Errorf("OPTIONS *: %d %q, want 401 %q", resp.StatusCode, body, unauthorized)
}
}
// TestHeaders: every response, whatever its status, carries the
// security headers, and none lets another origin in.
func TestHeaders(t *testing.T) {