Files
member-console/internal/httplimit/httplimit.go
T
cgalo5758 b245bdb0a9 Fix security audit findings from 2026-09-21 scan
Remediate six confirmed security issues: deployment-only config keys,
bounded provider responses, short-lived registration sessions, private
init file mode, FedWiki workflow authorization, and switch preview
gates.

- Add DeploymentOnly config key declaration; refuse runtime overrides
  for keys that decide where secrets are sent
- Create httplimit package; bound all provider response reads at 8 MiB
- Set fifteen-minute deadline on /register sessions
- Write mc-config.yaml with 0600 permissions
- Derive FedWiki workflow IDs from site IDs; re-authorize sites before
  mutating activities
- Apply switch authorization gates to the proration preview
2026-09-21 13:57:57 -05:00

79 lines
2.6 KiB
Go

// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Commercial
// SPDX-FileCopyrightText: 2025-2026 Christian Galo
// Package httplimit reads a response body from a configured provider within
// a byte bound. Every outbound client (FarmManager, Discourse, the Temporal
// OAuth token endpoint, and any client added later) reads through here, so
// a provider, or whoever stands where its URL points, cannot grow this
// process's heap without limit (security-audit-remediation-3 design D5).
//
// A bounded response is either complete or refused, never silently
// truncated: a truncated JSON document fails later with a worse message,
// and a truncated error body would misquote the provider.
package httplimit
import (
"encoding/json"
"errors"
"fmt"
"io"
"net/http"
)
// DefaultResponseBytes is the bound a client uses unless it knows its
// response is smaller. 8 MiB: the largest legitimate response today is
// FarmManager's site list, a few hundred bytes per site.
const DefaultResponseBytes int64 = 8 << 20
// TooLargeError reports a response that exceeded its bound. Host names the
// provider the response came from, so an operator reading the log knows
// which bound to raise, or which provider to distrust.
type TooLargeError struct {
Bound int64
Host string
}
func (e *TooLargeError) Error() string {
return fmt.Sprintf("response from %s exceeds %d bytes", e.Host, e.Bound)
}
// ReadResponse reads resp's body whole when it fits within max bytes and
// closes it either way. A body longer than max returns *TooLargeError and
// no bytes; reading stops at max+1, so the process never holds more than
// the bound for one response.
func ReadResponse(resp *http.Response, max int64) ([]byte, error) {
defer resp.Body.Close()
body, err := io.ReadAll(io.LimitReader(resp.Body, max+1))
if err != nil {
return nil, err
}
if int64(len(body)) > max {
return nil, &TooLargeError{Bound: max, Host: hostOf(resp)}
}
return body, nil
}
// DecodeResponse decodes resp's JSON body into v within the same bound
// ReadResponse applies, and closes the body. Beyond the bound it returns
// *TooLargeError and leaves v as the decoder left it.
func DecodeResponse(resp *http.Response, max int64, v any) error {
body, err := ReadResponse(resp, max)
if err != nil {
return err
}
return json.Unmarshal(body, v)
}
// IsTooLarge reports whether err is, or wraps, a TooLargeError.
func IsTooLarge(err error) bool {
var tl *TooLargeError
return errors.As(err, &tl)
}
func hostOf(resp *http.Response) string {
if resp.Request != nil && resp.Request.URL != nil {
return resp.Request.URL.Host
}
return "provider"
}