Skip to content

pages#template ignores its :version URL param, allowing wrong-app-type content to be permanently cached by the browser #1287

Description

@philayres

Summary

GET /pages/:version/template (PagesController#template) never reads the :version URL token. It renders content from the user's current state at the moment the AJAX request arrives, and stamps the response Cache-Control: private, max-age=604800, immutable. The browser caches by URL for 7 days without revalidation. If the user's app type changes between the page render (which embeds the token) and the deferred template AJAX, the wrong app type's templates are permanently bound to the URL for the correct app type.

Follow-up to #1279 / PR #1283 (which fixed the compiled-file cache-key scoping) and the viewable_tables cache-key fix. This is a narrower race, but produces the identical user-visible symptom: persistently missing master panel tabs / incomplete templates.

Mechanism

  1. Each full page load embeds _fpa.state.template_version = '<digest>' (app/views/layouts/_setup_app.html.erb). The digest (ApplicationHelper#template_version -> partial_cache_key(:loaded)) includes the user's app_type_id at page-render time.
  2. Later, asynchronously, _fpa.load_template_version fetches GET /pages/<token>/template with cache: true.
  3. PagesController#template ignores params[:id], computes the etag and renders masters/cache_search_results_template from the user's app type at AJAX time, and sets set_browser_cache(max_age: 604_800, immutable: true).
  4. The browser stores the response keyed by URL, and immutable prevents any revalidation for a week.

Concrete failure sequence (multi-tab)

  • t0: Tab 1 loads /masters/search in app type A. HTML embeds token V_A. The heavy template AJAX has not fired yet.
  • t1: Tab 2 switches app type via the navbar (/pages/home?use_app_type=B). AppTypeChange saves user.app_type_id = B (shared by all tabs).
  • t2: Tab 1's deferred AJAX fires: GET /pages/V_A/template. Server ignores V_A, sees app type B, returns app B's templates with max-age=604800, immutable. Browser caches B-content under the A URL.
  • t3: User switches back to app A. The new page embeds V_A again (same sign-in, same timestamps -> same digest). Browser serves the cached app B templates without contacting the server. Master panel shows app B's tabs; app A's tabs are missing.

The poisoning is durable within the session:

  • Revalidation cannot repair it: the etag in pages#template is also computed from current state, so when the user is back on app A the server's etag matches what the content should be and returns 304, confirming the browser's wrong body.
  • immutable means normal reloads never revalidate anyway; only a hard refresh or cache eviction clears it.

The same race can fire without a second tab: clicking the app selector while the first page is still loading its deferred template AJAX, or a logo_link/home redirect that embeds use_app_type and changes app type between page render and AJAX arrival.

Why this was previously hidden

Before #1270 (in 9.46.2 and earlier), partial_cache_key included user.updated_at, and every User save (including the app type switch itself) cleared the whole Rails cache and rotated server_cache_version. Switching A->B->A produced a brand-new URL every time, so a poisoned URL was never requested again. 9.46.3's stable tokens make URL reuse - and therefore permanent poisoning - possible.

Suggested fix

Make the server honor the URL/content contract in PagesController#template:

  • Compute the current template_version digest and compare with params[:id].
  • If they match: serve as now, with private, max-age=604800, immutable.
  • If they do not match: serve with Cache-Control: no-store (or redirect to the current-token URL) so mismatched content can never be cached under a long-lived immutable header.

A system spec should reproduce the race deterministically, e.g. render a page for app A, change the user's app type server-side before triggering the template fetch, and assert that the mismatched response is not cached / the correct tabs are shown after switching back.

Related

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions