53 lines
2.2 KiB
Go
53 lines
2.2 KiB
Go
package api
|
|
|
|
import (
|
|
"encoding/json"
|
|
"errors"
|
|
"mime"
|
|
"net/http"
|
|
"strings"
|
|
)
|
|
|
|
// maxBodyBytes caps request bodies; the API only accepts small JSON documents.
|
|
const maxBodyBytes = 1 << 20 // 1 MiB
|
|
|
|
// requireJSONContentType rejects a request whose body is not declared
|
|
// application/json, returning 415 before any decode. It guards the credential-bearing
|
|
// auth writes (email-OTP, passkey, op-login, setup redeem) against a cross-site
|
|
// forgery: an HTML form can
|
|
// only POST as application/x-www-form-urlencoded, multipart/form-data, or text/plain
|
|
// — never JSON — and a cross-site fetch that forces application/json triggers a CORS
|
|
// preflight this API never answers, so neither form can be forged off-origin. The
|
|
// session cookie's SameSite=Lax already blocks the bearing of credentials cross-site;
|
|
// this is the belt to that suspenders, and it costs a legitimate same-origin caller
|
|
// nothing (the panel always sends application/json on a bodied request). Media-type
|
|
// parameters (e.g. "; charset=utf-8") are ignored — only the type/subtype must match.
|
|
func requireJSONContentType(r *http.Request) error {
|
|
mt, _, err := mime.ParseMediaType(r.Header.Get("Content-Type"))
|
|
if err != nil || !strings.EqualFold(mt, "application/json") {
|
|
return newError(http.StatusUnsupportedMediaType, "unsupported_media_type",
|
|
"Content-Type must be application/json")
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// decodeJSON strictly decodes a small request body into v, rejecting unknown
|
|
// fields and trailing data so malformed callers fail fast with 400, and a body
|
|
// past maxBodyBytes with 413.
|
|
func decodeJSON(w http.ResponseWriter, r *http.Request, v any) error {
|
|
dec := json.NewDecoder(http.MaxBytesReader(w, r.Body, maxBodyBytes))
|
|
dec.DisallowUnknownFields()
|
|
if err := dec.Decode(v); err != nil {
|
|
var tooBig *http.MaxBytesError
|
|
if errors.As(err, &tooBig) {
|
|
return newError(http.StatusRequestEntityTooLarge, "too_large",
|
|
"request body is larger than %d bytes", tooBig.Limit)
|
|
}
|
|
return newError(http.StatusBadRequest, "bad_request", "invalid request body: %v", err)
|
|
}
|
|
if dec.More() {
|
|
return newError(http.StatusBadRequest, "bad_request", "unexpected trailing data in body")
|
|
}
|
|
return nil
|
|
}
|