REVIEW-001 (M0) perimeter findings: - H1: resolveForwarded walked X-Forwarded-For left→right and returned the leftmost untrusted entry. An append-style proxy (nginx's default) appends the peer it saw to the RIGHT of whatever the client sent, so the leftmost entry is attacker-controlled — a fresh rate-limit bucket per request (A3) and a forged client_ip in the audit trail (A11/D-031). Walk right→left and return the first untrusted entry, falling back to the peer. - L1s: the session cookie now carries the `__Host-` prefix when it is Secure (https), so it is pinned to the exact origin and cannot be shadowed from a sibling subdomain. Conditional on purpose: a browser rejects a `__Host-` cookie without Secure, which would break the http loopback bootstrap. - L4s: add Permissions-Policy, Cross-Origin-Opener-Policy and Cross-Origin-Resource-Policy. - Add httpx.IsLoopbackIP: a RESOLVED client IP test that fails closed on an empty/unparseable value (the setup-mode gate, H2), sharing a classifier with IsLoopbackHost whose "unknown" direction is the opposite, each fail-closed for its own decision. Tests updated: the unit test encoding the vulnerable leftmost expectation is rewritten, plus right→left/forged-entry/host-classification coverage.
57 lines
2.1 KiB
Go
57 lines
2.1 KiB
Go
// Package httpx holds the door's HTTP helpers: the session cookie, the client
|
|
// IP resolution (trusted proxies, A6), the security headers, log redaction, and
|
|
// the CSRF token (SPEC-003 §6/§8).
|
|
//
|
|
// It imports only net/http + the standard library — the door owns HTTP; the
|
|
// pure routing model (internal/core/routing) must never import this.
|
|
package httpx
|
|
|
|
import "net/http"
|
|
|
|
// SessionCookieName is the base name of the session cookie (SPEC-003 §6). The
|
|
// value is an opaque random id; the server-side session row is authoritative.
|
|
const SessionCookieName = "myday_session"
|
|
|
|
// HostPrefix is the `__Host-` cookie-name prefix (L1s). A browser only accepts
|
|
// a `__Host-` cookie when it is Secure · Path=/ · has no Domain attribute, so
|
|
// the cookie is pinned to the exact origin — a sibling subdomain cannot shadow
|
|
// it (a cookie-fixation / session-shadowing DoS).
|
|
const HostPrefix = "__Host-"
|
|
|
|
// SessionCookieNameFor returns the session cookie's name for the transport:
|
|
// `__Host-myday_session` over https (Secure is set — required by the prefix)
|
|
// and the bare name otherwise, because a browser REJECTS a `__Host-` cookie
|
|
// sent without Secure over plain http (the loopback bootstrap / human test).
|
|
func SessionCookieNameFor(secure bool) string {
|
|
if secure {
|
|
return HostPrefix + SessionCookieName
|
|
}
|
|
return SessionCookieName
|
|
}
|
|
|
|
// SetSessionCookie writes the hardened session cookie (SPEC-003 §6):
|
|
// HttpOnly · Secure · SameSite=Lax · Path=/. The lifetime is server-side, so
|
|
// no Max-Age is set (a browser-session cookie).
|
|
func SetSessionCookie(w http.ResponseWriter, name, value string, secure bool) {
|
|
http.SetCookie(w, &http.Cookie{
|
|
Name: name,
|
|
Value: value,
|
|
Path: "/",
|
|
HttpOnly: true,
|
|
Secure: secure,
|
|
SameSite: http.SameSiteLaxMode,
|
|
})
|
|
}
|
|
|
|
// ClearSessionCookie expires the session cookie (logout, or an invalid session).
|
|
func ClearSessionCookie(w http.ResponseWriter, name string, secure bool) {
|
|
http.SetCookie(w, &http.Cookie{
|
|
Name: name,
|
|
Value: "",
|
|
Path: "/",
|
|
HttpOnly: true,
|
|
Secure: secure,
|
|
SameSite: http.SameSiteLaxMode,
|
|
MaxAge: -1,
|
|
})
|
|
}
|