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 <script> (prefs/sources blobs).
This commit is contained in:
@@ -261,9 +261,12 @@ func (a *Authenticator) handleLogout(w http.ResponseWriter, r *http.Request) {
|
||||
http.Redirect(w, r, "/", http.StatusFound)
|
||||
}
|
||||
|
||||
// safeNext keeps post-login redirects on-site (no open redirects).
|
||||
// safeNext keeps post-login redirects on-site (no open redirects). It rejects
|
||||
// "//host" and "/\host" — browsers normalize the backslash to a slash, so the
|
||||
// latter would otherwise resolve to a protocol-relative off-site URL.
|
||||
func safeNext(next string) string {
|
||||
if next == "" || !strings.HasPrefix(next, "/") || strings.HasPrefix(next, "//") {
|
||||
if next == "" || !strings.HasPrefix(next, "/") ||
|
||||
strings.HasPrefix(next, "//") || strings.HasPrefix(next, "/\\") {
|
||||
return "/"
|
||||
}
|
||||
return next
|
||||
|
||||
@@ -82,6 +82,21 @@ type channelStat struct {
|
||||
Total int
|
||||
}
|
||||
|
||||
// jsForScript marks already-valid JSON as safe to embed in an inline <script>,
|
||||
// after neutralizing the byte sequences that could break out of that context
|
||||
// (</script>, HTML comments, and the JS line separators U+2028/U+2029). Without
|
||||
// this, a stored prefs blob containing "</script>" 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))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user