Fix push SSRF, cross-user unsub, and personalization edge cases
Code review of the personalization/feeds/PWA/push work surfaced ten confirmed issues, now fixed: - Web Push delivery bypassed the SSRF guard (unguarded default client); now routes through safehttp.NewClient with a hard timeout, and the subscribe handler validates the endpoint URL. - Push unsubscribe deleted by endpoint with no owner check; added RemovePushSubscriptionForUser scoped to the signed-in user. - Byte-slice body/content truncation could split a UTF-8 rune and break the RSS content:encoded XML; added a rune-safe truncateUTF8 helper. - Digest sender could permanently starve a user who hid a high-volume source; step the watermark past a full hidden-source scan window. - Service worker cached personalized HTML navigations into a shared cache (identity leak across PWA users); navigations are now network-only, CACHE_VERSION bumped to v2 to purge stale pages. - Public /api/article leaked discarded/unclassified bodies; filter to classified, non-sentinel stories. - runLocal never started the push sender; digests now fire in -local. - Push client had no timeout, so one hung endpoint stalled all sends. - Reader migration resurrected cross-device-cleared reads; gate it behind a one-time flag so the server stays authoritative. - Bookmarks count didn't match the classified list filter.
This commit is contained in:
@@ -414,10 +414,7 @@ func extractBodyText(doc *goquery.Document) string {
|
||||
out = alt
|
||||
}
|
||||
}
|
||||
if len(out) > MaxBodyChars {
|
||||
out = out[:MaxBodyChars]
|
||||
}
|
||||
return out
|
||||
return truncateUTF8(out, MaxBodyChars)
|
||||
}
|
||||
|
||||
// bodyContainerSelectors is the list of selectors we try when <article>/<main>
|
||||
|
||||
@@ -10,6 +10,7 @@ import (
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
"unicode/utf8"
|
||||
|
||||
"github.com/mmcdole/gofeed"
|
||||
ext "github.com/mmcdole/gofeed/extensions"
|
||||
@@ -174,8 +175,24 @@ func extractContentText(raw string) string {
|
||||
}
|
||||
s = manyNewlineRe.ReplaceAllString(strings.Join(lines, "\n"), "\n\n")
|
||||
s = strings.TrimSpace(s)
|
||||
if len(s) > maxContentChars {
|
||||
s = s[:maxContentChars]
|
||||
return truncateUTF8(s, maxContentChars)
|
||||
}
|
||||
|
||||
// truncateUTF8 caps s at max bytes without splitting a multi-byte rune. A plain
|
||||
// byte slice can leave a partial trailing rune, which is invalid UTF-8; that
|
||||
// corrupt byte then breaks the RSS content:encoded XML (Go's xml encoder writes
|
||||
// it raw) and shows as a replacement char in JSON. Trim back to a rune boundary.
|
||||
func truncateUTF8(s string, max int) string {
|
||||
if len(s) <= max {
|
||||
return s
|
||||
}
|
||||
s = s[:max]
|
||||
for len(s) > 0 {
|
||||
if r, size := utf8.DecodeLastRuneInString(s); r == utf8.RuneError && size <= 1 {
|
||||
s = s[:len(s)-1] // strip one byte of an incomplete trailing rune
|
||||
continue
|
||||
}
|
||||
break
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user