Address code review findings across API, web and deployment
release gate / check (push) Failing after 6m43s
release gate / check (push) Failing after 6m43s
High severity: - wire TRUSTED_PROXY through Compose/deploy/docs and warn at web startup - bucket public rate limits on effective client IP, add per-bookmark budget, and forward the signed browser identity from the share page - disable first-run bootstrap in server deployments and document seeding - serialize last-admin demotion in one advisory-locked transaction Medium/low: - cap and batch share recipient inserts - add a shared outbound budget for preview/CardDAV/IMAP/signal work - pin CardDAV dials to 443 and bound IMAP logout - fix rightward kanban column drags and normalize cards on column delete - classify backend outages in auth guards, add request timeouts and an error boundary, remove the dead login route, surface throttling - enforce pinned/unpinned page reorder grouping server-side - avoid preview work for stale bookmark versions; drop duplicate dialog fetch - sequence-guard collection/tag/calendar/quick-find loads - validate property config and fix MULTI_SELECT/DATE filter inputs - document index maintenance; apply same-origin check to sign-out - trim signup names, unique shared dates and kanban keys (migration 010), reset reference cache on identity changes, hide OIDC issuer, return 413 for oversized direct bodies, escape Markdown link destinations - remove dead template files and duplicate test helper Verified with go build/vet, full PostgreSQL race suite, db convergence, tsc, both ESLint configs and a Next production build.
This commit is contained in:
@@ -13,6 +13,7 @@ import (
|
||||
"net/http"
|
||||
"net/url"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
"unicode/utf8"
|
||||
@@ -36,7 +37,19 @@ func read(w http.ResponseWriter, r *http.Request) (object, bool) {
|
||||
r.Body = http.MaxBytesReader(w, r.Body, 1<<20)
|
||||
var b object
|
||||
dec := json.NewDecoder(r.Body)
|
||||
if dec.Decode(&b) != nil || b == nil {
|
||||
if err := dec.Decode(&b); err != nil {
|
||||
// Review L-8: an oversized body is 413, not malformed JSON. The Next
|
||||
// proxy already answers 413; direct API callers deserve the same
|
||||
// class of error.
|
||||
var tooLarge *http.MaxBytesError
|
||||
if errors.As(err, &tooLarge) {
|
||||
writeJSON(w, 413, Error{"Request body too large (limit 1 MiB)"})
|
||||
return nil, false
|
||||
}
|
||||
writeJSON(w, 400, Error{"Invalid JSON object"})
|
||||
return nil, false
|
||||
}
|
||||
if b == nil {
|
||||
writeJSON(w, 400, Error{"Invalid JSON object"})
|
||||
return nil, false
|
||||
}
|
||||
@@ -98,6 +111,27 @@ func ids(b object, k string) ([]string, error) {
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// Share-list caps (review M-1). A 1 MiB JSON body can carry tens of
|
||||
// thousands of ids; each one used to become an INSERT in a user-locked
|
||||
// transaction. These are product limits, not storage limits.
|
||||
const (
|
||||
maxShareRecipients = 100
|
||||
maxSharedDateRecipients = 200
|
||||
)
|
||||
|
||||
// cappedIDs is ids plus a length cap (review M-1), returning a clear 400.
|
||||
func cappedIDs(b object, key string, max int) ([]string, error) {
|
||||
list, e := ids(b, key)
|
||||
if e != nil {
|
||||
return nil, e
|
||||
}
|
||||
if len(list) > max {
|
||||
return nil, bad(fmt.Sprintf("%s may contain at most %d entries", key, max))
|
||||
}
|
||||
return list, nil
|
||||
}
|
||||
|
||||
func fields(b object, allowed string) object {
|
||||
out := object{}
|
||||
for _, k := range strings.Fields(allowed) {
|
||||
@@ -254,11 +288,23 @@ func reorder(r *http.Request, table, col string, order []string) error {
|
||||
return nil
|
||||
})
|
||||
}
|
||||
func shareRows(r *http.Request, tx *sql.Tx, table, key, id string, list []string) error {
|
||||
for _, u := range list {
|
||||
if _, e := tx.ExecContext(r.Context(), `INSERT INTO `+db.Quote(table)+` (`+db.Quote(key)+`,"userId") VALUES($1,$2) ON CONFLICT DO NOTHING`, id, u); e != nil {
|
||||
return e
|
||||
}
|
||||
|
||||
// outboundBusy answers 429 with Retry-After when the shared outbound budget
|
||||
// is exhausted (review M-2).
|
||||
func outboundBusy(w http.ResponseWriter, retry time.Duration) {
|
||||
if retry < time.Second {
|
||||
retry = time.Second
|
||||
}
|
||||
return nil
|
||||
w.Header().Set("Retry-After", strconv.Itoa(int(retry.Seconds())+1))
|
||||
writeJSON(w, 429, Error{"Too much outbound work right now; try again shortly"})
|
||||
}
|
||||
|
||||
// shareRows inserts every recipient in ONE statement (review M-1). table and
|
||||
// key come only from the server-side call sites, never from request input.
|
||||
func shareRows(r *http.Request, tx *sql.Tx, table, key, id string, list []string) error {
|
||||
if len(list) == 0 {
|
||||
return nil
|
||||
}
|
||||
_, e := tx.ExecContext(r.Context(), `INSERT INTO `+db.Quote(table)+` (`+db.Quote(key)+`,"userId") SELECT $1,unnest($2::text[]) ON CONFLICT DO NOTHING`, id, list)
|
||||
return e
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user