From d5d0656dbaac2d4f25353fa1dec07867d310d1b4 Mon Sep 17 00:00:00 2001 From: prosolis <5590409+prosolis@users.noreply.github.com> Date: Sun, 21 Jun 2026 16:21:03 -0700 Subject: [PATCH] Harden security: SSRF guard on all outbound fetches, auth fixes - Route image-validation, Matrix image-download, and wayback/archive.today fetches through safehttp so feed-controlled URLs can't reach loopback/ RFC1918/cloud-metadata IPs (incl. via redirects). - Cap feed body size with safehttp.LimitedBody (16 MiB) to prevent OOM. - Fail closed if matrix.pickle_key is unset/<16 chars; drop the hardcoded "pete_pickle_key" default that silently weakened E2EE-at-rest. - Gate !post behind a matrix.admins allowlist; empty = disabled (channel rooms are public, so empty must not mean anyone). - Reject /\host open-redirect bypass in post-login safeNext. - Allowlist http(s) schemes for the Matrix link href; escape JSON embedded in inline , HTML comments, and the JS line separators U+2028/U+2029). Without +// this, a stored prefs blob containing "" could inject markup. +func jsForScript(b []byte) template.JS { + r := strings.NewReplacer( + "<", "\\u003c", + ">", "\\u003e", + "&", "\\u0026", + "
", "\\u2028", + "
", "\\u2029", + ) + return template.JS(r.Replace(string(b))) +} + func (s *Server) base(r *http.Request) pageData { srcJSON, err := json.Marshal(s.sources) if err != nil { @@ -91,7 +106,7 @@ func (s *Server) base(r *http.Request) pageData { SiteTitle: s.cfg.SiteTitle, Channels: channels, Weather: currentWeather(time.Now()), - AllSources: template.JS(srcJSON), + AllSources: jsForScript(srcJSON), AuthEnabled: s.auth != nil, UserPrefs: template.JS("null"), Path: r.URL.Path, @@ -100,7 +115,7 @@ func (s *Server) base(r *http.Request) pageData { if u := s.auth.userFromRequest(r); u != nil { d.User = u if blob, err := storage.GetUserPrefs(u.Sub); err == nil && blob != "" { - d.UserPrefs = template.JS(blob) + d.UserPrefs = jsForScript([]byte(blob)) } } } diff --git a/main.go b/main.go index cf5d7f9..d432874 100644 --- a/main.go +++ b/main.go @@ -88,11 +88,25 @@ func main() { // Wire reaction handler mx.SetReactionHandler(poster.HandleReaction) + // Allowlist of users permitted to run in-room commands. The channel rooms + // are public, so commands are gated to configured admins; an empty list + // disables them entirely (fail closed) rather than allowing anyone. + admins := make(map[id.UserID]bool, len(cfg.Matrix.Admins)) + for _, a := range cfg.Matrix.Admins { + if a = strings.TrimSpace(a); a != "" { + admins[id.UserID(a)] = true + } + } + // Wire !post command: force-publish next queued story for the room's channel. mx.SetMessageHandler(func(channel string, roomID id.RoomID, eventID id.EventID, sender id.UserID, body string) { if strings.TrimSpace(body) != "!post" { return } + if !admins[sender] { + slog.Warn("!post ignored: sender not in matrix.admins allowlist", "channel", channel, "sender", sender) + return + } slog.Info("!post requested", "channel", channel, "sender", sender) if queue.ForcePost(channel) { return