All checks were successful
check / check (push) Successful in 3m46s
Two places decided whether a request was TLS, by two different means, and they disagreed. The session cookie's Secure attribute was fixed at startup from !Config.IsDev(). "dev" is the environment when WEBHOOKER_ENVIRONMENT is unset, so a deployment terminating TLS at a proxy without also setting the environment shipped the authentication cookie with no Secure attribute -- on the same response as a CSRF cookie that had one. It failed silently: everything kept working, so nothing prompted anyone to look. The CSRF middleware's per-request check compared X-Forwarded-Proto with == "https" exactly, so "HTTPS", "https, http" and "https,https" all took the plaintext path. Uppercase is legal for a case-insensitive token and the comma forms are what a proxy chained behind another proxy emits by appending rather than replacing. On that path gorilla/csrf stops enforcing the strict Referer check on a site that genuinely is HTTPS. Both now go through internal/reqtls.IsTLS, which folds case and takes the leftmost comma-separated element -- the hop nearest the client, and so the one a cookie's Secure attribute is about. A third package is needed because internal/middleware already imports internal/session, so session cannot import middleware back. Per-request beat a startup warning for the session cookie because it turned out to need no restructuring: gorilla/sessions gives every session its own copy of the store's Options and renders the cookie from that copy, and every session-cookie write here already goes through Session.Save or Session.Regenerate, both of which hold the request. The store's template Secure becomes true so that a write path added later which forgets to track the transport fails visibly instead of silently dropping Secure. The flag tracks the transport in both directions rather than latching on. Secure over plaintext is discarded by the browser without an error, which would make a plain-HTTP local run impossible to log into -- and would also void the deletion cookies in Destroy and Regenerate, leaving a session the user just tried to end still live. A third site that makes this decision, internal/handlers' BaseURL construction, assigns the raw header straight into the URL scheme. It is left alone here and filed separately.
145 lines
4.1 KiB
Go
145 lines
4.1 KiB
Go
package middleware
|
|
|
|
import (
|
|
"context"
|
|
"net/http"
|
|
"time"
|
|
|
|
httpmetrics "github.com/slok/go-http-metrics/metrics"
|
|
)
|
|
|
|
// MetricsMiddlewareForTest builds the metrics recording middleware
|
|
// against a caller-supplied recorder, so a test can gather from its
|
|
// own Prometheus registry rather than the process-wide default one
|
|
// that Middleware.Metrics uses.
|
|
func MetricsMiddlewareForTest(
|
|
rec httpmetrics.Recorder,
|
|
) func(http.Handler) http.Handler {
|
|
return metricsMiddleware(rec)
|
|
}
|
|
|
|
// UnmatchedRouteConst exposes the sentinel that stands in for a
|
|
// request matching no route pattern.
|
|
const UnmatchedRouteConst = unmatchedRoute
|
|
|
|
// InflightHandlerConst exposes the fixed handler label on the
|
|
// inflight gauge.
|
|
const InflightHandlerConst = inflightHandler
|
|
|
|
// UnmatchedMethodConst exposes the sentinel that stands in for a
|
|
// method the router can never route.
|
|
const UnmatchedMethodConst = unmatchedMethod
|
|
|
|
// NewLoggingResponseWriterForTest wraps newLoggingResponseWriter
|
|
// for use in external test packages.
|
|
func NewLoggingResponseWriterForTest(
|
|
w http.ResponseWriter,
|
|
) *loggingResponseWriter {
|
|
return newLoggingResponseWriter(w)
|
|
}
|
|
|
|
// LoggingResponseWriterStatusCode returns the status code
|
|
// captured by the loggingResponseWriter.
|
|
func LoggingResponseWriterStatusCode(
|
|
lrw *loggingResponseWriter,
|
|
) int {
|
|
return lrw.statusCode
|
|
}
|
|
|
|
// IPFromHostPort exposes ipFromHostPort for testing.
|
|
func IPFromHostPort(hp string) string {
|
|
return ipFromHostPort(hp)
|
|
}
|
|
|
|
// ClientKeyForTest exposes clientKey for testing.
|
|
func ClientKeyForTest(m *Middleware, r *http.Request) string {
|
|
return m.clientKey(r)
|
|
}
|
|
|
|
// LoginRateLimitConst exposes the loginRateLimit constant: the
|
|
// number of FAILED login attempts one client may make against one
|
|
// submitted username per interval.
|
|
const LoginRateLimitConst = loginRateLimit
|
|
|
|
// LoginFailureMaxKeysConst exposes the cap on each of the login
|
|
// guard's key sets.
|
|
const LoginFailureMaxKeysConst = loginFailureMaxKeys
|
|
|
|
// PasswordVerifyConcurrencyConst exposes the bound on concurrent
|
|
// Argon2id verifications.
|
|
const PasswordVerifyConcurrencyConst = passwordVerifyConcurrency
|
|
|
|
// PasswordVerifyMaxWaitersConst exposes the bound on how many
|
|
// requests may queue for a verification slot.
|
|
const PasswordVerifyMaxWaitersConst = passwordVerifyMaxWaiters
|
|
|
|
// LoginGuard is the login failure counter and verification
|
|
// semaphore, exposed for direct testing.
|
|
type LoginGuard = loginGuard
|
|
|
|
// NewLoginGuardForTest builds a guard with test-sized parameters.
|
|
func NewLoginGuardForTest(
|
|
limit int,
|
|
interval time.Duration,
|
|
maxKeys, concurrency, maxWaiters int,
|
|
wait time.Duration,
|
|
) *LoginGuard {
|
|
return newLoginGuard(
|
|
limit, interval, maxKeys, concurrency, maxWaiters, wait,
|
|
)
|
|
}
|
|
|
|
// QueuedWaitersForTest reports how many requests are currently
|
|
// queued for a verification slot.
|
|
func (g *LoginGuard) QueuedWaitersForTest() int {
|
|
return len(g.queue)
|
|
}
|
|
|
|
// SetNowForTest replaces the guard's clock.
|
|
func (g *LoginGuard) SetNowForTest(now func() time.Time) {
|
|
g.mu.Lock()
|
|
defer g.mu.Unlock()
|
|
|
|
g.now = now
|
|
}
|
|
|
|
// FailForTest exposes fail.
|
|
func (g *LoginGuard) FailForTest(clientKey, username string) bool {
|
|
return g.fail(clientKey, username)
|
|
}
|
|
|
|
// SucceedForTest exposes succeed.
|
|
func (g *LoginGuard) SucceedForTest(clientKey, username string) {
|
|
g.succeed(clientKey, username)
|
|
}
|
|
|
|
// AcquireForTest exposes acquire.
|
|
func (g *LoginGuard) AcquireForTest(
|
|
ctx context.Context,
|
|
) (func(), bool) {
|
|
return g.acquire(ctx)
|
|
}
|
|
|
|
// TrackedKeysForTest reports how many failure counters the guard
|
|
// holds, per-username and per-address respectively.
|
|
func (g *LoginGuard) TrackedKeysForTest() (int, int) {
|
|
g.mu.Lock()
|
|
defer g.mu.Unlock()
|
|
|
|
return len(g.byUser), len(g.byAddr)
|
|
}
|
|
|
|
// PasswordChangeRateLimitConst exposes the
|
|
// passwordChangeRateLimit constant.
|
|
const PasswordChangeRateLimitConst = passwordChangeRateLimit
|
|
|
|
// ReceiverAggregateMultiplierConst exposes the
|
|
// receiverAggregateMultiplier constant.
|
|
const ReceiverAggregateMultiplierConst = receiverAggregateMultiplier
|
|
|
|
// ReceiverAggregateLimitForTest exposes receiverAggregateLimit for
|
|
// testing.
|
|
func ReceiverAggregateLimitForTest(perEntrypoint int) int {
|
|
return receiverAggregateLimit(perEntrypoint)
|
|
}
|