Commit Graph

2948 Commits

Author SHA1 Message Date
gudvinr 9a774083ab refactor(response): use http.Header for header map
http.Header is built for headers, so use it for its intended purpose
v2.3.1 2.3.1
2026-05-28 16:13:04 -07:00
dependabot[bot] 79381e6f9a build(deps): bump the gomod group with 4 updates
Bumps the gomod group with 4 updates: [github.com/go-webauthn/webauthn](https://github.com/go-webauthn/webauthn), [golang.org/x/crypto](https://github.com/golang/crypto), [golang.org/x/image](https://github.com/golang/image) and [golang.org/x/net](https://github.com/golang/net).


Updates `github.com/go-webauthn/webauthn` from 0.17.3 to 0.17.4
- [Release notes](https://github.com/go-webauthn/webauthn/releases)
- [Changelog](https://github.com/go-webauthn/webauthn/blob/master/CHANGELOG.md)
- [Commits](https://github.com/go-webauthn/webauthn/compare/v0.17.3...v0.17.4)

Updates `golang.org/x/crypto` from 0.51.0 to 0.52.0
- [Commits](https://github.com/golang/crypto/compare/v0.51.0...v0.52.0)

Updates `golang.org/x/image` from 0.40.0 to 0.41.0
- [Commits](https://github.com/golang/image/compare/v0.40.0...v0.41.0)

Updates `golang.org/x/net` from 0.54.0 to 0.55.0
- [Commits](https://github.com/golang/net/compare/v0.54.0...v0.55.0)

---
updated-dependencies:
- dependency-name: github.com/go-webauthn/webauthn
  dependency-version: 0.17.4
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: gomod
- dependency-name: golang.org/x/crypto
  dependency-version: 0.52.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: gomod
- dependency-name: golang.org/x/image
  dependency-version: 0.41.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: gomod
- dependency-name: golang.org/x/net
  dependency-version: 0.55.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: gomod
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-05-27 17:35:18 -07:00
Frédéric Guillot 161ed71eb0 fix(api): forbid setting OAuth identity fields via user update
A non-admin could bind an arbitrary OAuth identity to their own account
by patching google_id or openid_connect_id, bypassing the duplicate
check enforced in the OAuth callback. Remove both fields from the
update request so binding only happens through the OAuth flow.
2026-05-26 21:04:56 -07:00
Frédéric Guillot c896bafdaa fix(urllib): reject backslashes in relative path validation
Browsers normalize backslashes to forward slashes, so a redirect target
like "/\evil.com" parsed as a relative path by url.Parse and resolved to
//evil.com by the browser, resulting in an open redirect. Reject any link
containing a backslash before validating it as a relative path.
2026-05-26 20:06:58 -07:00
gudvinr 66996e3ffa refactor(storage): use same API for list and singular items
Instead of relying on WithX and WithXs just stick to latter with parametrized arguments.
2026-05-26 19:21:25 -07:00
jvoisin f050f23bda refactor(storage): collapse WithTags into a single array-containment predicate
WithTags previously emitted one "LOWER($i) = ANY(LOWER(e.tags::text)::text[])"
condition and one parameter per filter tag. With K tags this means K
predicates, K parameters, and K evaluations of the LOWER(e.tags::text)::text[]
sub-expression per candidate row.

This commit replaces the loop with a single predicate using the array
containment operator:

    LOWER(e.tags::text)::text[] @> LOWER($N::text)::text[]

Same case-insensitive "row must contain all listed tags" semantics, fewer
predicates for the planner, and the row-side expression is referenced once
instead of K times.
2026-05-26 19:08:38 -07:00
jvoisin 50088405e0 perf(reader): walk feed <link> tags in a single DOM pass
findSubscriptionsFromWebPage was running four separate goquery.Find passes, one
per supported MIME type. This commit replaces it with one
doc.Find("link[type]") traversal and a switch on the type attribute, which is
functionally equivalent, but it visits the DOM once instead of four times and
emits results in document order. Microbenchmark on a representative <head> (8
link tags + ~100 unrelated head children):

  before: ~25 µs/op, 1536 B/op, 50 allocs/op
  after:  ~9  µs/op,  744 B/op, 21 allocs/op
2026-05-25 20:01:25 -07:00
gudvinr d235a63138 refactor(sanitizer): always trim spaces in StripTags
It's never used without being trimmed anyway
2026-05-25 18:07:09 -07:00
jvoisin 483da488d8 perf(finder): size findSubscriptionsFromWebPage dedup map 2026-05-24 17:50:27 -07:00
gudvinr e01a6100ca fix(readingtime): make CJK detection more reliable
* division by 50 is 2%, not 50%
* non-letters are often the same between LTR languages
2026-05-24 17:40:08 -07:00
gudvinr a725476164 fix(readingtime): trim CJK text by rune not by bytes
Common mistake when working with UTF-8 is to use sub-slicing for truncate. That splits multi-byte runes in half breaking encoding.
2026-05-24 17:40:08 -07:00
jvoisin 47e304f343 perf(integration): don't defer in a for loop
Since the Body isn't used, it can immediately be closed, instead of deferring
the operation to the end of the function.
2026-05-23 21:35:47 -07:00
gudvinr 9dfed35946 refactor(storage): consistent construction of query builders
Make sure there's only one way to create new builder
2026-05-23 21:31:44 -07:00
gudvinr 95f5f1e77d refactor(storage): use query builder as builder
As query builders declared as such embrace this to the full extent.
2026-05-23 21:31:44 -07:00
gudvinr a08f598cc8 refactor(storage): return entryPaginationBuilder from builder methods 2026-05-23 21:31:44 -07:00
jvoisin 0e1523551b perf(finder): optimize a tad findCanonicalURL
- Don't call strings.TrimSpace twice on canonicalHref
- Use doc.FindMatcher+goquery.Single instead of doc.Find+First, as is done
  everywhere else in the codebase.
2026-05-23 20:58:55 -07:00
jvoisin eff9502462 refactor(storage): use INNER JOIN where LEFT JOIN is redundant
Replace LEFT JOIN with INNER JOIN in queries where the WHERE clause or
foreign key constraints already guarantee matching rows exist:

- Icons(): filters on feeds.user_id
- UserByAPIKey(): filters on api_keys.token
- fetchFeedCounter(): filters on feeds columns when counterJoinFeeds is set
- fetchEntries(): entries have FK constraints to feeds, categories, and
  users (feed_icons/icons kept as LEFT JOIN since icons are optional)

PostgreSQL already optimizes these identically, but INNER JOIN makes the
intent explicit.
2026-05-23 20:50:18 -07:00
jvoisin ab94f42ad1 refactor(storage): Use INNER JOIN instead of LEFT JOIN in IconByFeedID
The WHERE clause on feeds columns already eliminates NULL-extended rows,
making the LEFT JOINs logically equivalent to INNER JOINs. PostgreSQL's
planner is smart enough to recognize this and produces an identical
execution plan (verified with EXPLAIN ANALYZE), but using INNER JOIN
makes the intent explicit for humans like me reading the query.
2026-05-23 20:42:26 -07:00
gudvinr f66772e911 feat(sanitizer): speed up TruncateHTML by a lot 2026-05-23 18:15:36 -07:00
gudvinr 43456daddf feat(sanitizer): TruncateHTML benchmarks 2026-05-23 18:15:36 -07:00
gudvinr 863d6039ad refactor(sanitizer): split common stripping code into iterator function 2026-05-23 18:15:36 -07:00
gudvinr fc3e548be7 refactor(sanitizer): move truncate tests to single table 2026-05-23 18:15:36 -07:00
Harold Kim 096a15ef14 feat(locale): add Korean translation 2026-05-23 18:07:11 -07:00
jvoisin 0976efd163 refactor(readingtime): get rid of the obnoxious casts dance 2026-05-19 20:26:52 -07:00
Frédéric Guillot 06e36c3e54 Revert "feat(ui): add stdlib cross-origin protection middleware"
This reverts commit deef74e75b.
2026-05-17 20:40:39 -07:00
jvoisin 39772c33f0 perf(subscription): use a slice instead of a map for well-known feed paths
findSubscriptionsFromWellKnownURLs iterates a fixed table of 9 well-known
feed paths against the discovered base URLs. The table was declared as a
map[string]string, which paid per-iteration hash overhead and gave
non-deterministic probe order across runs.

This commit replaces it with a fixed-size [...]struct{path, format string}.
Probes now run in declared order (more predictable behavior for users when
multiple well-known URLs respond, and easier to reason about in tests), and the
inner loop avoids the map iterator entirely.

Micro-benchmark replaying the double loop body against 2 base URLs ×
9 paths, sans HTTP I/O (medians of 5 runs):

    name           old ns/op    new ns/op    delta
    KnownURLs      24,194       18,150       -25%

    name           old B/op     new B/op     delta
    KnownURLs      9,688        9,072        -6.4%

    name           old allocs/op  new allocs/op  delta
    KnownURLs      110            107            -3
2026-05-17 19:16:00 -07:00
jvoisin 098270cb14 refactor(misc): various minor code simplifications
- No need to use `make(…)` construct for empty slices, as is already the case
  in the rest of internal/googlereader/handler.go
- Replace a useless condition in internal/reader/sanitizer/sanitizer.go with an
  unconditional assignment.
- Remove a useless call to url.Parse in JoinBaseURLAndPath, as url.JoinPath
  already performs the validation internally
2026-05-17 19:07:37 -07:00
jvoisin 24c65304a7 perf(date): cache timezone Locations for PST/PDT/EST/EDT fallback
parseLocalTimeDates is called once per parsed feed entry. Each call to
time.LoadLocation("America/Los_Angeles" | "America/New_York") reads and
parses the IANA tzdata file from disk or from the embedded zoneinfo.

This commit hoists the two LoadLocation calls to package-level vars so the
lookup happens once at init time and the hot path becomes a pointer load.

Benchmarked on the local-time fallback path:
  before: ~24,000 ns/op, ~15 KB/op, 26 allocs/op
  after:  ~330    ns/op, 0  B/op,   0  allocs/op
2026-05-17 19:06:47 -07:00
jvoisin 3747e686af security(metrics): use constant-time comparison for metrics endpoint credentials
The metrics Basic Auth check used != for username and password, which is
technically vulnerable to timing side-channel attacks. Since the metrics
credentials are typically short static config values, the timing difference is
very likely to be in the noise level, but oh well, it's a good practise to do
credential validation in constant time.
2026-05-17 19:05:52 -07:00
jvoisin cc1d39efe1 perf(misc): preallocate some slices 2026-05-16 20:42:33 -07:00
jvoisin bdd7f4f365 perf(database): drop two redundant indexes
entries_feed_idx(feed_id) is covered by both the unique constraint
entries_feed_id_hash_key(feed_id, hash) and the explicit index
entries_feed_id_status_hash_idx(feed_id, status, hash), which handle
all feed_id-leading lookups including FK cascade deletes.

entries_user_status_idx(user_id, status) is a prefix of five existing
three-column indexes (entries_user_status_feed_idx,
entries_user_status_changed_idx, entries_user_status_published_idx,
entries_user_status_created_idx, entries_user_status_changed_published_idx),
all of which serve every query the two-column index could.

Saves ~14 MB per million entries.
2026-05-16 19:32:43 -07:00
gudvinr 7628a214f3 fix(storage): remove possible SQL injection
As ORDER BY strings can't be included in parametrized queries, queries containing them are vulnerable to SQL injections.
2026-05-16 19:20:12 -07:00
jvoisin 20545d28e9 refactor(storage): remove a useless ORDER BY in GetEnclosure
The query selects a single row by primary key, there is no need to sort
anything.
v2.3.0 2.3.0
2026-05-13 19:23:01 -07:00
dependabot[bot] 771977407c build(deps): bump the gomod group with 6 updates
Bumps the gomod group with 6 updates:

| Package | From | To |
| --- | --- | --- |
| [github.com/go-webauthn/webauthn](https://github.com/go-webauthn/webauthn) | `0.17.2` | `0.17.3` |
| [golang.org/x/crypto](https://github.com/golang/crypto) | `0.50.0` | `0.51.0` |
| [golang.org/x/image](https://github.com/golang/image) | `0.39.0` | `0.40.0` |
| [golang.org/x/net](https://github.com/golang/net) | `0.53.0` | `0.54.0` |
| [golang.org/x/term](https://github.com/golang/term) | `0.42.0` | `0.43.0` |
| [golang.org/x/text](https://github.com/golang/text) | `0.36.0` | `0.37.0` |


Updates `github.com/go-webauthn/webauthn` from 0.17.2 to 0.17.3
- [Release notes](https://github.com/go-webauthn/webauthn/releases)
- [Changelog](https://github.com/go-webauthn/webauthn/blob/master/CHANGELOG.md)
- [Commits](https://github.com/go-webauthn/webauthn/compare/v0.17.2...v0.17.3)

Updates `golang.org/x/crypto` from 0.50.0 to 0.51.0
- [Commits](https://github.com/golang/crypto/compare/v0.50.0...v0.51.0)

Updates `golang.org/x/image` from 0.39.0 to 0.40.0
- [Commits](https://github.com/golang/image/compare/v0.39.0...v0.40.0)

Updates `golang.org/x/net` from 0.53.0 to 0.54.0
- [Commits](https://github.com/golang/net/compare/v0.53.0...v0.54.0)

Updates `golang.org/x/term` from 0.42.0 to 0.43.0
- [Commits](https://github.com/golang/term/compare/v0.42.0...v0.43.0)

Updates `golang.org/x/text` from 0.36.0 to 0.37.0
- [Release notes](https://github.com/golang/text/releases)
- [Commits](https://github.com/golang/text/compare/v0.36.0...v0.37.0)

---
updated-dependencies:
- dependency-name: github.com/go-webauthn/webauthn
  dependency-version: 0.17.3
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: gomod
- dependency-name: golang.org/x/crypto
  dependency-version: 0.51.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: gomod
- dependency-name: golang.org/x/image
  dependency-version: 0.40.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: gomod
- dependency-name: golang.org/x/net
  dependency-version: 0.54.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: gomod
- dependency-name: golang.org/x/term
  dependency-version: 0.43.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: gomod
- dependency-name: golang.org/x/text
  dependency-version: 0.37.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: gomod
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-05-13 16:42:27 -07:00
Fred 0642e8a3ea fix(webauthn): persist backup flags
Store WebAuthn backup eligibility and backup state with each
credential instead of overwriting BackupEligible from every login
assertion.

Use a nullable backup_eligible column to identify legacy credentials
and backfill those records on their next successful login. Also persist
the validated credential state after login, including sign count, clone
warning, and backup state.
2026-05-11 20:29:27 -07:00
Fred 059ec55f52 security(webauthn)!: require discoverable passkeys
Remove the username-based WebAuthn login flow because it allowed
username enumeration before password verification.

WebAuthn login now uses discoverable credentials only, and new
registrations require resident keys. Existing non-resident credentials
are no longer usable for first-factor login; they should only be used
in a post-password MFA flow, which Miniflux does not currently
implement.

BREAKING CHANGE: Users with existing non-resident WebAuthn credentials
must register a new passkey.
2026-05-11 19:41:02 -07:00
dashitongzhi 9fd6f44311 fix: typo in wallabag integration error message
Change "unable to get save entry" to "unable to save entry"
in the wallabag createEntry error message and its corresponding
test assertion.
2026-05-11 19:39:33 -07:00
jvoisin 018128e109 fix(storage): delete orphaned icons
The icons table is deduplicated by hash and shared with feeds via the
feed_icons junction. When a feed is deleted, the ON DELETE CASCADE
removes its feed_icons row but leaves the icons row behind. The same
happens in StoreFeedIcon when a feed's icon is replaced. Over time
these orphaned bytea blobs accumulate and bloat the database.

Add Storage.CleanupOrphanIcons, which deletes icons rows that no
feed_icons row still references, and call it from runCleanupTasks.
2026-05-11 17:57:32 -07:00
gudvinr 3f05747a78 refactor(internal): use errors.Is instead of equality operator
errors.Is unwraps nested error. This makes check less error prone.
2026-05-11 17:40:12 -07:00
jvoisin 7a5b4b109e fix(googlereader): avoid inlining validateApiKey at every call site
The middleware (*authMiddleware).validateApiKey is registered for 14
routes in NewHandler. Its body was just `return http.HandlerFunc(func…)`,
which made the compiler consider it inlinable. As a result, the entire
losure body was duplicated at every call site, taking space in the .text
section.

Move the request-handling logic into a separate non-inlined method
serveValidated and keep validateApiKey as a thin wrapper that only
allocates the http.HandlerFunc. The 14 duplicated symbols are gone and
the stripped binary shrinks from 20,513,033 to 20,447,497 bytes, which isn't
that much, but it's still something, especially for such a simple commit.
2026-05-11 16:38:54 -07:00
jvoisin c2661ca1c4 perf(processor): avoid per-char string allocation in parseISO8601Duration
Replace `num += string(char)` with index-based slicing of the input
string. The previous loop allocated a new string for every digit in
the duration (and again on each `+=`), giving O(n²) allocation
behavior. Slicing `after[start:i]` reuses the original string's
backing memory and only allocates once per numeric component when
passed to `strconv.Atoi`.

It also makes the code a bit more compact/simpler.

Called once per YouTube/podcast entry that exposes an ISO8601
duration during feed processing.
2026-05-11 13:55:52 -07:00
Frédéric Guillot 30ad65e03b refactor(webauthn): tighten finishLogin and saveCredential
Surface previously-swallowed storage errors, remove a dead UserByID
lookup, and simplify the discoverable login control flow.
2026-05-09 21:20:05 -07:00
Frédéric Guillot 9a0e357842 refactor(webauthn): clean up variable naming
Drop the snake_case cred_uid, fix the missing-n typos in webAuthUser
and webAuthCredential, replace credCredential with validatedCredential,
unify uid/userID on userID, rename the shadowed url local in
newWebAuthn to baseURL, and use the full credential / credentials names
throughout instead of the abbreviated cred / creds. No behaviour change.
2026-05-09 20:52:18 -07:00
Frédéric Guillot d4f362ad31 refactor(webauthn): drop unreachable uid==0 guards
The web session middleware redirects unauthenticated requests to the
login page before any non-public handler runs, so request.UserID is
guaranteed non-zero in beginRegistration, finishRegistration, and
deleteCredential. Remove the dead checks to match the other WebAuthn
handlers.
2026-05-09 20:29:04 -07:00
jvoisin 10bdbf82b9 fix(webauthn): check ownership in saveCredential on WebAuthn rename
While both deleteCredential either validate or pass down a uid,
saveCredential doesn't. This isn't exploitable as an authenticated attacker
would need to guess the 32 bytes handle of another one, but it doesn't hurt to
explicitly check if a user is only operating on their own user.
2026-05-09 18:21:21 -07:00
Frédéric Guillot f467c2daa1 refactor(ui): require POST for OAuth2 unlink endpoint
Account unlinking mutates state, so /oauth2/{provider}/unlink can no
longer be reached via GET. Pull the OAuth2/OIDC and WebAuthn sections
out of the settings form and render each as its own fieldset above the
form, with the unlink action as a self-contained POST form. Rename
the username/password fieldset legend to "Password Authentication" and
add matching legends for the federated and passkey sections so all
authentication methods read consistently.
2026-05-09 18:19:59 -07:00
jvoisin 6b06c9f4f4 perf(readingtime): don't allocate words to count them
The function strings.Fields will allocate every single word it's creating,
meaning that for a text of 10k words, 10k allocations will be made, only for
them to be counted an discarded. We can do much better by counting the words
ourself via a small countWords helper function, and write a test to prove that
it doesn't allocate anything.
2026-05-09 16:29:33 -07:00
Frédéric Guillot 30ede1caa1 refactor(ui): require POST for feed refresh endpoints
Feed refresh endpoints mutate state and should not be reachable via
GET. Drop the GET registrations on /feeds/refresh, /feed/{id}/refresh,
/category/{id}/feeds/refresh and /category/{id}/entries/refresh, and
update the templates and the R keyboard shortcut to submit POST forms
with a CSRF token.
2026-05-08 21:08:51 -07:00
Frédéric Guillot 16902a2297 refactor(logout): require POST for /logout
Logout is a state-changing action and should not be reachable via GET.
Switching to POST routes the request through the CSRF middleware so
prefetchers and cross-site GETs can no longer terminate the session.
2026-05-08 20:39:06 -07:00
jvoisin 08de9546e3 fix(csrf): apply CSRF to all non-safe methods
Better safe than sorry
2026-05-08 16:15:44 -07:00