Skip to content

v0.9.6

Latest

Choose a tag to compare

@github-actions github-actions released this 04 Aug 16:27
· 1 commit to main since this release
Immutable release. Only release title and notes can be modified.
v0.9.6
20fe5aa

Security

  • Feed error messages no longer expose server network details. When a feed
    fails to fetch, the reason shown in the sidebar tooltip is read by every
    subscriber of that feed. It was the raw Go error, which embeds whatever the
    connection resolved to — e.g. dial tcp 10.0.0.5:443: connect: connection refused, handing any user a piece of your internal network map. Subscribers
    now see a plain summary ("could not connect to the server", "the server
    responded 404", "the server's TLS certificate could not be verified"), while
    the complete error is still written to the server log for operators. The
    mapping is fail-closed: an unrecognised error reports a generic message
    rather than falling back to the raw text.
  • Bumped golang.org/x/text to 0.40.0, picking up the fix for GO-2026-5970
    (an infinite loop on invalid input). Ember reached the affected code when
    draining an Ollama model-pull response.
  • Repeated failed logins are now throttled per account, not just per IP.
    Ember already rate-limited the login endpoint per source address, which does
    nothing against a credential-stuffing run spread across many addresses. Each
    username now also gets an escalating backoff: the first five consecutive
    failures are free, after which every further attempt has to wait — 1s, 2s,
    4s, doubling up to a one-minute ceiling — returned as 429 with a
    Retry-After header. A successful sign-in clears it immediately. This is
    deliberately a delay and not a lockout: a hard lock would let anyone who
    knows your username keep you out of your own reader. Failed attempts past the
    free allowance are now logged with the username and client IP so an attack in
    progress is visible.
  • A passkey whose signature counter stops advancing is now rejected. That
    is the WebAuthn spec's signal of a cloned authenticator or a replayed
    assertion; Ember previously recorded it and signed the user in anyway.
    Authenticators that don't implement a counter at all (most phones and
    laptops) are unaffected, as the spec requires.
  • Hardened the login endpoint against memory exhaustion. Every attempt runs
    a 64 MiB argon2id derivation — including attempts for usernames that don't
    exist — and nothing bounded how many could run at once, so enough concurrent
    attempts could push the process into an out-of-memory kill. Derivations are
    now capped at four in flight; the rest queue.
  • The login rate-limiter's internal table is now bounded, so traffic spread
    across a large address range can't grow it without limit.
  • Closed two SSRF bypasses in the private-address block. http://[::]/ and
    IPv4-compatible addresses like http://[::127.0.0.1]/ slipped past the guard
    that stops Ember fetching internal hosts, because Go only normalizes the
    IPv4-mapped form (::ffff:a.b.c.d) when comparing against IPv4 ranges — so
    the loopback check never matched, while the connection itself reached
    localhost. The whole ::/96 range is now blocked (nothing routable lives
    there). Affected every outbound fetch: adding a feed, discovery, the poller,
    and the image proxy.
  • Web Push notifications had a data race that could corrupt payloads. A
    single encoded notification was shared across the goroutines fanning out to
    each of your devices, and the push library writes into the spare capacity of
    the buffer it's handed — so concurrent sends could scribble over each other.
    Each send now gets its own copy.
  • Directory listings are no longer served for static asset paths. Requesting
    /assets/ returned an index of every built file, and because that path also
    carries a year-long immutable cache header, the listing was cached as if it
    were a content-hashed asset. Directory requests now return 404; single-page-app
    routes are unaffected.
  • Digest emails now use an unpredictable MIME boundary. The boundary was
    guessable, and article titles are written into the plain-text part unescaped,
    so a crafted title could close the part early and forge additional MIME
    sections in the message. Boundary generation now fails the send outright
    rather than falling back to a fixed value.

Changed

  • The interface stays responsive while feeds are being fetched. Ember opens
    a single SQLite connection because the database allows only one writer, which
    meant every read — the article list, sidebar counts, search — had to queue
    behind whatever the background poller happened to be writing. Reads now go
    through a second, read-only connection pool that runs alongside the writer, so
    a refresh in progress no longer stalls the UI. Worst-case read latency during
    a feed fetch drops from ~166ms to ~23ms, roughly 1.45x faster on a typical
    sidebar load. Note for operators: this raises Ember's SQLite page-cache
    budget from 64 MiB to about 128 MiB (the read pool is deliberately capped at
    four connections of 16 MiB rather than inheriting the writer's 64 MiB each).
    If the read pool can't be opened for any reason, Ember logs a warning and
    serves reads from the write connection exactly as before.
  • Bumped Go runtime dependencies: github.com/mmcdole/gofeed 1.3.0 → 1.4.0
    (which moves to goxpp/v2 and drops five transitive dependencies),
    github.com/pressly/goose/v3 3.27.2 → 3.27.3, golang.org/x/crypto
    0.53.0 → 0.54.0, golang.org/x/net 0.56.0 → 0.57.0, and
    modernc.org/sqlite 1.53.0 → 1.55.0.
  • Bumped SPA build/dev tooling: Svelte 5.56.4 → 5.56.8, Vite 8.1.3 → 8.2.0,
    @sveltejs/vite-plugin-svelte 7.1.4 → 7.2.0, svelte-check 4.7.1 → 4.7.4,
    @testing-library/jest-dom 6 → 7, @types/node 26.1.0 → 26.1.2, jsdom
    29 → 30.0.1, and @playwright/test 1.61.1 → 1.62.1. These are dev-only and are
    not bundled into the Ember binary. TypeScript is deliberately held at 6.x:
    svelte-check 4.7.4 supports TypeScript 7 only with both TypeScript 6 and 7
    installed side by side and an extra --tsgo flag, which is a build-tooling
    migration rather than a version bump.

Fixed

  • Scheduled OPML exports written before noon UTC are no longer lost. The
    export filename was built from a Go time layout that had .opml folded into
    it, and Go reads the pm in that extension as the AM/PM marker — so every
    export produced between 00:00 and 11:59 UTC was written as .oaml instead.
    Those files landed on disk but never matched the .opml filter behind the
    admin export list, leaving them invisible in Settings → Database and
    impossible to download or delete from the UI. Affects 0.9.4 and 0.9.5;
    existing .oaml files can be renamed to .opml to bring them back into the
    list.
  • Articles no longer stay hidden waiting for their AI summary. When
    summaries are enabled, a new article was held back until the model had
    finished with it — with no time limit. On CPU-only inference that meant
    minutes of an apparently empty reader, and an article dropped from a full
    summary queue stayed invisible until Ember was restarted. Articles now appear
    once they've waited longer than a grace window (2 minutes by default), with
    the summary filling in when it's ready. Tune it in Settings → Language
    model → Article visibility
    or with EMBER_SUMMARY_GRACE_SECONDS; 0 shows
    articles as soon as they're fetched. (#162)
  • Articles dropped from a busy summary queue are picked back up. The queue
    is bounded, and when it was full the article was silently skipped — it then
    had no summary, so the gate above hid it, and the only thing that retried was
    a sweep that ran once at startup. That sweep now runs every poll cycle.
  • Turning on the daily digest works again. Saving from Settings → Digest
    always failed with a generic "invalid request body" error, so the feature
    could not be enabled at all from the UI. Ember was rejecting a request its
    own interface had built: the settings it sends back on save included two
    read-only fields (user_id, last_sent_at) that the save endpoint didn't
    accept. The endpoint now accepts and ignores them — so fetching your digest
    settings, changing one, and saving them back works from any client — and the
    interface no longer sends them in the first place. Which account the settings
    belong to is still taken from your session, never from the request, so the
    accepted field can't be used to change someone else's digest.
    Thanks to @Zaptorg for the report and the root-cause analysis (#161).
  • The All Unread badge could drift below the real count while you read.
    Marking an article read from Starred, Read Later, Shared, or a board
    decremented the All Unread badge even when that article was older than the
    unread window — so the badge subtracted something it had never counted, and
    showed fewer unread articles than the All Unread list actually contained.
    Those four views are deliberately not windowed (they show saved items of any
    age), which is why only they were affected. The badge self-corrected on the
    next server refresh, but that refresh is debounced and restarts on every
    read, so while you were scrolling through a list it stayed wrong.
  • Newsletter mail addressed to several of your inboxes was being dropped.
    The inbound SMTP server overwrote the recipient on each RCPT TO instead of
    collecting them, so when one message was addressed to multiple Ember inboxes
    only the last one received it. Nothing bounced — the sender saw a normal
    success — so the loss was silent.
  • Feeds that return unparseable content no longer get retried forever. Only
    fetch failures widened the retry interval; a feed that fetched fine but
    failed to parse kept being re-requested at the floor interval indefinitely
    (~48 requests a day at the default, aimed at someone else's origin). Parse
    failures now back off on the same curve as fetch failures.
  • OPML import now reports how many subscriptions it actually added. It
    previously counted every feed in the file, so re-importing the same OPML
    claimed to have added everything again when it had added nothing.
  • Backup and OPML-export retention only touches files Ember created. The
    prune and delete paths matched any .db / .opml file in the configured
    directory. Since both directories are admin-settable, pointing one at a
    directory holding the live ember.db could have deleted the database in use.
    They now match only Ember's own ember-<timestamp>.db / .opml naming.

Added

  • Turn AI summaries off for a single feed. Feeds you skim don't need a
    summary, and on CPU-only inference they cost minutes of work each. The feed's
    menu in the sidebar gains Don't summarize — the feed keeps updating
    normally, it just stops being sent to the model, and its existing summary
    cards disappear from your view. Switching it back on re-queues the articles
    that were skipped while it was off, so you don't have to wait for the feed to
    publish something new. The entry only appears while AI summaries are enabled
    on the server. It's a per-account choice: because a summary is stored once and
    shared by everyone subscribed to a feed, Ember only skips the inference when
    every subscriber has opted out — otherwise the work still happens and your
    copy simply arrives without the summary card. The "Summarizing N articles"
    indicator no longer counts work for feeds you've opted out of.

  • Optional device verification for passkey sign-in. A new Require device
    verification
    toggle in Settings → Passkeys (or EMBER_PASSKEY_REQUIRE_UV=1)
    makes passkey sign-in demand a PIN, fingerprint, or face scan, so a passkey
    becomes two factors rather than possession alone. Off by default, because a
    passkey enrolled on a security key with no PIN configured would stop working
    and need re-registering — turn it on once you know every registered passkey
    can verify. Phones and laptops (Touch ID, Windows Hello) always verify, so
    they're unaffected either way.

What's Changed

  • chore(deps): apply the grouped Go + web bumps, holding TypeScript at 6.x by @brandonhon in #169
  • web,api: stop the All Unread badge drifting below its list by @brandonhon in #167
  • auth,api,store: throttle login brute force and harden passkey assertions by @brandonhon in #166
  • refactor(internal): consolidate duplication, fix 8 latent bugs, raise coverage by @brandonhon in #168
  • chore(deps-dev): bump postcss from 8.5.15 to 8.5.23 in /docs by @brandonhon in #178
  • chore(deps): catch up Go, web, and GitHub Actions dependencies by @brandonhon in #181
  • ci(deps): bump pinned GitHub Actions, keeping the codeql trio in lockstep by @brandonhon in #187
  • store,db: serve heavy read-only queries from a separate WAL read pool by @brandonhon in #188
  • docs(changelog): record the read/write connection split by @brandonhon in #189
  • release: v0.9.6 by @brandonhon in #194

Full Changelog: v0.9.5...v0.9.6