Skip to content

fix(core): OpenSearch searches from the address bar, Chrome's size limit, suggestions and caching - #5386

Merged
imorland merged 19 commits into
2.xfrom
fix/opensearch-hardening
Nov 1, 2026
Merged

imorland merged 19 commits into
2.xfrom
fix/opensearch-hardening

Conversation

@imorland

@imorland imorland commented Nov 1, 2026

Copy link
Copy Markdown
Member

Fixes: no issue filed. Follow-up to #5321 and #5328.

Changes proposed in this pull request:

A review of OpenSearch, and driving it in headless Chromium against a dev forum, turned up searches from the address bar that went wrong, a forum Chrome never added, and suggestions that missed obvious titles.

Searches from the address bar:

  • Spaces and true/false. Browsers send a space as + (Chromium escapes {searchTerms} with EscapeQueryParamValue(…, true)), and Mithril's query parser keeps the +. ?q=hello+world showed hello+world in the search box, and sorting or loading more searched for it; only the first page, rendered by the server, was right. Mithril also turns true and false into booleans: a search for true threw in searchToQuery and left the discussion list empty, and one for false was ignored. GlobalSearchState now reads q from the address with URLSearchParams.
  • An address that doesn't change. The description's search address depended on the homepage, but browsers keep the one they were given: after the homepage changed, searches landed on a page that ignored them. It is now /opensearch/search?q=, which redirects to the discussion list wherever it is, with spaces as %20.
  • The homepage at its own address. With /all as the homepage, /all?q=… (an engine saved while another page was the homepage) fell back to / without the query. The frontend now moves the address to the root and keeps the query, with or without a trailing slash.

The description:

  • Chrome's size limit. Chrome ignores a description over 50,000 bytes (max_body_size in TemplateURLFetcher). An uploaded .ico was embedded as it was: a 60KB one made the document 81KB, and Chrome added nothing. Images that don't fit are left out, and Chrome then shows /favicon.ico. An .ico also declares its own size rather than 16×16.
  • Control characters in the title or description made it ill-formed. They are left out.
  • DOM rather than XMLWriter, which core doesn't require and some distributions package separately. DOM is an install requirement already.
  • No image work per request. The favicon was read, resized and encoded on every request. The images that fit are cached for a day, as data: URIs, by where the favicon is stored, which changes when it is replaced.

Caching:

  • /opensearch was public, max-age=3600, and the forum adds the visitor's session cookie and CSRF token to it. A shared cache that honoured public could hand one visitor's session to others. It is private now, and so are /manifest and the /favicon.ico redirect, which had the same problem before OpenSearch.

Suggestions:

  • The query is echoed as typed. Chrome drops a response whose first element differs from the input, so a trailing space dropped them. A q that isn't text suggests nothing.
  • Only the first 8 words are matched. Each word added two LIKE conditions, so a guest could build a query SQLite refuses with a 500.
  • Words are completed after punctuation: "solv" finds "[Solved] …", "fibre" finds "carbon-fibre". The database finds words at the start of a title or after a space; when there are fewer than 8, it finds up to 100 recent titles containing the words, and where they start is checked in PHP, ignoring case and accents as MySQL, MariaDB and PostgreSQL do.
  • On SQLite, whose LIKE folds ASCII only, accented words match in any case ("über" finds "Über…").

Reviewers should focus on:

  • Chrome's "tab to search" can't be turned on by a site. Chrome adds an engine from a description only on a page whose path is /, and since TemplateURLData::ActiveStatus it adds it as inactive (is_active=0). Inactive engines can't be used for keyword search (CanBeUsedForKeywordMatching) until someone activates them under "Inactive shortcuts" in chrome://settings/searchEngines. Headless Chromium 156 stored the forum with the right search and suggestion addresses and is_active=0. Forums in a subfolder are never added automatically.
  • Choosing a suggestion searches for its title; Chrome and Firefox ignore the addresses in the response.
  • The suggestion matching was tested on SQLite only. Accent-insensitive matching on MySQL, MariaDB and PostgreSQL follows from their collations and ILIKE; the PHP check now agrees with them.
  • OpenSearchDescription takes a cache repository in its constructor, and searchPage() is public.

Necessity

  • Has the problem that is being solved here been clearly explained?
  • If applicable, have various options for solving this problem been considered?
  • For core PRs, does this need to be in core, or could it be in an extension?
  • Are we willing to maintain this for years / potentially forever?

Confirmed

  • Frontend changes: tested on a local Flarum installation. (Headless Chromium against a dev forum: 19 checks, including Chrome adding the forum with a 60KB .ico.)
  • Frontend changes: tests are green (run yarn test in js/).
  • Frontend changes: tests have been added, or are not appropriate here.
  • Backend changes: tests are green (run composer test).
  • Backend changes: tests have been added, or are not appropriate here.
  • Where applicable, changes are suitable for all supported database drivers (MySQL, MariaDB, PostgreSQL, SQLite). (Tested on SQLite only.)
  • Core developer confirmed locally this works as intended.
  • The description above is written by me and describes what this pull request actually does.

imorland and others added 19 commits November 1, 2026 15:12
Browsers drop suggestions whose first element differs from the text typed,
so a trailing space dropped them. A query that isn't text suggests nothing
rather than searching for "Array".
…s query

Every word added two LIKE conditions, so a guest could build a query
SQLite refuses with a 500, and load any database.
Words were only completed at the start of a title or after a space, so
"solv" didn't find "[Solved] …" and "fibre" didn't find "carbon-fibre".
… on SQLite

SQLite's LIKE folds the case of ASCII letters only, so "über" didn't find
"Über…".
XML can't contain them, so a title pasted with one made the document
ill-formed and browsers refused it.
XMLWriter is a separate PHP extension that core doesn't require, and some
distributions package it on its own. DOM is already an install requirement.
An uploaded .ico is embedded as it is, and a 60KB one made the document
81KB. Chrome ignores a description over 50,000 bytes, so it never added
the forum. Images that don't fit are left out.
…ts size

It was always declared 16x16, whatever sizes it held.
…hange

The search address depended on the homepage, but browsers keep the one they
were given: after the homepage changed, searches landed on a page that
ignored them. /opensearch/search sends them on to the discussion list
wherever it is now, with spaces encoded as %20 rather than the "+" browsers
send, which the frontend kept.
The forum adds the visitor's session cookie and CSRF token to it, which a
cache honouring "public" could hand to other visitors.
…rect

Like the OpenSearch description, they carry the visitor's session cookie
and CSRF token, which a cache honouring "public" could hand to others.
…request

The favicon was read from the assets disk, decoded, resized and encoded as
an .ico each time. It is kept for a day by where it is stored, which
changes when it is replaced.
… address

A browser's address bar sends ?q=hello+world, and Mithril kept the "+":
the search box showed it, and sorting or loading more searched for it.
Mithril also turns "true" and "false" into booleans, so a search for
"true" broke the discussion list and one for "false" was ignored.
The homepage is served at the root and the frontend has nothing at its own
address, so the router fell back to the root without the query: a search a
browser saved as /all?q=… lost it once /all became the homepage.
MySQL, MariaDB and PostgreSQL find "Café" for "cafe", but the check for
where words start compared accents, and dropped it.
…others

Only the 100 most recently active titles containing the words were checked
for words starting there, so on a busy forum newer titles with the word
inside another ("art" in "smart") hid older ones that start with it.
…h a trailing slash

The router strips the slash only after the address is checked, so /all/?q=…
still lost its query.
The cache held the favicon's raw bytes, however big, before images too big
for Chrome were left out. It now holds only the images that fit, as text,
which every cache store can keep.
@imorland
imorland marked this pull request as ready for review November 1, 2026 16:20
@imorland
imorland requested a review from a team as a code owner November 1, 2026 16:20
@imorland imorland added this to the 2.1 milestone Nov 1, 2026
@imorland
imorland merged commit de1d83c into 2.x Nov 1, 2026
49 of 50 checks passed
@imorland
imorland deleted the fix/opensearch-hardening branch November 1, 2026 16:46
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants