Refuse API writes that are not application/json (closes #271)
check / check (push) Failing after 3s

A page on another origin of the same site could send a text/plain or
form POST with the user's cookie, since a browser sends such a request
without a preflight. RequireJSON on the /api/v1 router answers 415 to a
POST, PUT or DELETE whose Content-Type is not application/json, or that
has none, before the handler runs.

The web client and neoirc-cli already send the header on every request.

Model: opus-5-5
This commit is contained in:
sneak committed 2026-11-11 01:33:10 +00:00
1 parent ba79d6de02
commit 2be34bcedf
5 files changed
+255 -3

No files matched your search

+52
View File
@@ -3,6 +3,7 @@ package middleware
import (
"log/slog"
"mime"
"net"
"net/http"
"net/netip"
@@ -217,6 +218,57 @@ func (mware *Middleware) CORS() func(http.Handler) http.Handler {
})
}
// notJSONError is the body of the 415 answer, in the API's usual
// error format.
const notJSONError = `{"error":"content type must be application/json"}` + "\n"
// needsJSON reports whether a request with method must have the
// Content-Type application/json, whether or not it has a body.
func needsJSON(method string) bool {
return method == http.MethodPost ||
method == http.MethodPut ||
method == http.MethodDelete
}
// isJSON reports whether the request's Content-Type is
// application/json, with or without parameters such as charset.
func isJSON(request *http.Request) bool {
mediaType, _, err := mime.ParseMediaType(
request.Header.Get("Content-Type"),
)
return err == nil && mediaType == "application/json"
}
// RequireJSON returns middleware that answers 415 to a POST, PUT or
// DELETE request whose Content-Type is not application/json, or that
// has none. A browser sends a request with another type, such as
// text/plain, to another origin of the same site without asking the
// server first, and attaches the auth cookie. Refusing those types
// stops a page on that origin from acting as the signed-in user.
func (mware *Middleware) RequireJSON() func(http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(
func(
writer http.ResponseWriter,
request *http.Request,
) {
if needsJSON(request.Method) && !isJSON(request) {
writer.Header().Set(
"Content-Type",
"application/json; charset=utf-8",
)
writer.WriteHeader(http.StatusUnsupportedMediaType)
_, _ = writer.Write([]byte(notJSONError))
return
}
next.ServeHTTP(writer, request)
})
}
}
// Metrics returns middleware that records HTTP metrics.
func (mware *Middleware) Metrics() func(http.Handler) http.Handler {
metricsMiddleware := ghmm.New(ghmm.Config{ //nolint:exhaustruct // optional fields