From 60c35389673833dc13da43f1e1c49703db17091d Mon Sep 17 00:00:00 2001 From: dev_agent Date: Sat, 3 Oct 2026 10:08:46 -0400 Subject: [PATCH] Reject session lifetimes finer than stored millisecond precision MEALQUEST_SESSION_IDLE and MEALQUEST_SESSION_MAX must now be whole milliseconds: session expiry is stored as a store.Timestamp, so a value such as 1500us would be truncated and a session could expire early. Fixes review finding MQ-19-1. Co-Authored-By: Claude Opus 5.5 --- internal/config/config.go | 16 ++++++++++++++-- internal/config/config_test.go | 14 ++++++++++++++ 2 files changed, 28 insertions(+), 2 deletions(-) diff --git a/internal/config/config.go b/internal/config/config.go index d091bd4..699003b 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -86,8 +86,8 @@ func load(lookup func(string) (string, bool)) (Config, error) { OIDCClientSecret: r.secretFile(EnvOIDCClientSecretFile), OIDCAllowedGroups: r.list(EnvOIDCAllowedGroups), - SessionIdle: r.optionalDuration(EnvSessionIdle, DefaultSessionIdle), - SessionMax: r.optionalDuration(EnvSessionMax, DefaultSessionMax), + SessionIdle: r.sessionDuration(EnvSessionIdle, DefaultSessionIdle), + SessionMax: r.sessionDuration(EnvSessionMax, DefaultSessionMax), } if cfg.SessionIdle > 0 && cfg.SessionMax > 0 && cfg.SessionMax < cfg.SessionIdle { r.fail(EnvSessionMax, "must be at least %s (%s)", EnvSessionIdle, cfg.SessionIdle) @@ -277,6 +277,18 @@ func (r *reader) optionalDuration(name string, def time.Duration) time.Duration return r.parseDuration(name, v) } +// sessionDuration is optionalDuration restricted to whole milliseconds: +// session expiry is stored as a store.Timestamp, which keeps millisecond +// precision, so a finer duration could not be honoured. +func (r *reader) sessionDuration(name string, def time.Duration) time.Duration { + d := r.optionalDuration(name, def) + if d%time.Millisecond != 0 { + r.fail(name, "duration must be a whole number of milliseconds, got %s", d) + return 0 + } + return d +} + func (r *reader) parseDuration(name, v string) time.Duration { d, err := time.ParseDuration(v) if err != nil { diff --git a/internal/config/config_test.go b/internal/config/config_test.go index bd8feae..4dafec1 100644 --- a/internal/config/config_test.go +++ b/internal/config/config_test.go @@ -269,6 +269,20 @@ func TestLoad(t *testing.T) { env: with(map[string]string{EnvSessionMax: "-1h"}), badVars: []string{EnvSessionMax}, }, + { + name: "whole-millisecond session lifetimes", + env: with(map[string]string{EnvSessionIdle: "1ms", EnvSessionMax: "2ms"}), + }, + { + name: "sub-millisecond session idle", + env: with(map[string]string{EnvSessionIdle: "1500us", EnvSessionMax: "2ms"}), + badVars: []string{EnvSessionIdle}, + }, + { + name: "sub-millisecond session max", + env: with(map[string]string{EnvSessionIdle: "1ms", EnvSessionMax: "1ms500us"}), + badVars: []string{EnvSessionMax}, + }, { name: "session max below idle", env: with(map[string]string{EnvSessionIdle: "48h", EnvSessionMax: "24h"}),