Contents
An attack arrives as a link someone opens, not as a call your app makes.
In requests
- Requests under
/_serverFn/that are browser navigations (aSec-Fetch-Mode: navigateheader, ortext/htmlinAccept) or that lack thex-tsr-serverFn: trueheader your own client sends. These are exactly what Appwrite's edge rules block. inference from the mitigations8 - A GET to a server-function path carrying its payload in the query string, arriving from a link in an email, a chat or another site rather than from your own pages.
After a click
- Actions a user doesn't remember taking, made with their own session from their own browser, since the script makes authenticated requests as them. inference from the impact
- Nothing on the server itself: the advisory describes no server compromise.4
What isn't known yet
The sources give no date for when Lovable found the issue or reported it, and neither TanStack nor GitHub says either way whether it was exploited before 30 September. Treat those as open questions, not as a clean bill of health.
A JSON endpoint was talked into serving a web page.
The bug sits where data from a browser becomes state inside the server. TanStack Start trusted the decoded payload as if it were its own internal object, so fields a caller added travelled into places only the server should set. One of those places decided how an error reply was labelled, and a reply labelled HTML is something a browser runs.
Scroll, or focus the stage and use the arrow keys. The amber dot follows the link's payload; the camera moves to the part each step is about. Field names are conceptual; there is no payload here.
- Mass assignmentCopying every field of client data into an internal object instead of picking the allowed ones. Also called over-posting.
- Reflected XSSCross-site scripting where the attacker's input travels in the request (here, a link) and the server sends it straight back in a page the browser runs.
- CWE-79The catalogue entry for cross-site scripting: input that ends up in a web page without being neutralized.
A server function is an HTTP endpoint on your own origin.
TanStack Start lets you write server functions: code that runs on the server, which your pages call over HTTP. By default the call goes to an internal endpoint under
/_serverFn/, with the function's id in the path and the headerx-tsr-serverFn: true.The endpoint lives on your app's own origin, next to your pages and your session cookie. A server function can be called by POST or by GET, with the payload in the query string.4
- path
- /_serverFn/<id>
- header
- x-tsr-serverFn: true
- methods
- GET or POST
- origin
- your app's own
The payload should carry one public field:
data.The server decodes the serialized payload with Seroval's
fromJSON, runs the middleware and the function, then serializes the result back to the browser.The only field meant to cross from the browser is
data, the function's argument. Anything else in the object was written by whoever built the request, which need not be your page.- decoder
- Seroval fromJSON
- public field
- data
- set by the server
- context, method
- other fields
- chosen by the caller
Before the fix, the whole object crossed the boundary.
The handler took the entire decoded payload, set
contextandmethodon it, and passed it on. InsidecreateServerFnthat object was spread (...opts) into the internal middleware context, so every extra field became internal state.This is mass assignment: trusting a client-built object as if the server had built it, instead of copying across only the allowed fields. The advisory puts it plainly: payload fields can reach “internal middleware state instead of limiting them to public input fields.”4
- handed on
- the whole payload
- in createServerFn
- ...opts spread in
- extra fields
- became internal state
- should cross
- data only
An error path built its reply from that state.
The handler picked the function's
resultover itserror, and had a “not found” branch that built an HTTP response from fields of the result object. It setContent-Type: application/jsonfirst and spread the suppliedheadersafter it, so a caller-supplied value could override the JSON default.our reading of the fix diff The header order comes from the fix's diff. The advisory itself says only that there was “an error path where attacker content persists.”2
- picked
- result over error
- first
- Content-Type: application/json
- then
- ...headers from the result
- last writer
- the caller
Server functions answer GET, so the payload fits in a link.
Because a server function can be called by GET with its payload in the query string, the whole crafted request fits in one URL on your site. The attacker sends that link by email, in a chat or from another website.
Building it needs no account and no access to your app. What it does need is a victim who opens it: that is the “user interaction required” in the score.4
- method
- GET
- payload
- in the query string
- account needed
- no
- victim must
- open the link
The reply comes back as HTML, from your own origin.
The victim's browser navigates to
https://your-app/_serverFn/…. The reply really does come from your app, and it is labelled with an HTML content type, so the browser renders it as a page.Labelled
application/json, the same bytes would have been shown as inert text. One header decided whether the attacker's content was data or a document.- expected
- application/json, shown as text
- served
- text/html
- from
- your app's own URL
- browser
- renders it as a page
Script runs with the victim's session.
Because the page is on your origin, the attacker's script runs with the same access as the signed-in user: it can read same-origin data and page contents, and make authenticated requests as them. Cookies without the
HttpOnlyflag are exposed too.That is the “scope changed” in the vector: the flaw is in the server, but the code runs in the victim's browser. Any public app on an affected version, on React, Solid or Vue, had such an endpoint. No server compromise is described.4
- runs as
- the signed-in user
- can
- read same-origin data
- can
- make authenticated requests
- cookies
- exposed unless HttpOnly
- server
- not compromised
The fix: an allowlist on the way in, JSON on the way out.
Commit d521abd71c (pull request #8573, merged 30 Sep at 17:29:55 UTC) builds the action's input explicitly as
{ data, context, method }in the HTTP handler, the SSR path and the serialization adapter. Nothing else from the payload crosses.It also prefers the error over the result, forces
Content-Type: application/jsonon not-found replies after applying headers, returns non-object results only when they are real responses or primitives, and turns any other handler output into a generic 500.2- input
- { data, context, method }
- error vs result
- error first
- not-found reply
- JSON, forced last
- other output
- generic 500
The numbered markers on the diagram match the steps.
[The fix] restricts client-supplied input and validates responses at the server boundary.
Disclosed on 30 September 2026. Some answers aren't public yet.
This advisory went public on 30 September 2026, and this page was written from sources checked that same day. The fix and the version numbers are settled. Several things are not.
- Fixed versionsPublished on npm, 17:48–17:50 UTC
- SeverityCritical, CVSS 3.1 9.3 (GitHub advisory)
- NVD and CVE.orgNo record yet
- Exploited in the wildNo statement either way
- Found and reportedDates not public
- CISA KEVNot listed
Facts may still change
An NVD record, a listing in CISA's Known Exploited Vulnerabilities catalog or a report of exploitation would each be news. Upgrading now means none of them can catch you out.
The fix lives one package down from the one you import.
Your app depends on @tanstack/react-start, solid-start or vue-start. The vulnerable code is in @tanstack/start-server-core, a dependency they pull in. A bumped framework package with a stale lockfile can still resolve an old core, which is why TanStack's post asks you to check that the core itself resolves to 1.169.39 or later.1
The four packages also number their releases independently. Each range starts at 1.143.12, but each ends somewhere different, so a version number means nothing until you know which package it belongs to.
- Transitive dependencyA package you get through another package rather than by naming it yourself. It still appears in the lockfile.
- Rebuild and redeployServer code is bundled when the app is built, so a new lockfile changes nothing until a new build is running.
Nine months in the releases. Under an hour from fix to advisory.
- ~9months
from the first affected release (27 Dec 2025) to the fix.
- ~25
minor and patch releases shipped inside the vulnerable range.
- 21min
from the fix's pull request opening (17:09) to its merge (17:29:55).
- 27min
from the merge to the public advisory at 17:57:08 UTC.
A fix that didn't say it was one
The pull request was titled “fix(start): align server function transport behavior” and its changeset “Align server function request and response handling”. Neither says security. Merging and releasing a fix shortly before the advisory goes public is a common pattern in coordinated disclosure; it means only an advisory feed tells you in time.
Critical, even though someone has to click.
A 9.3 for a bug that needs a victim to open a link can look high. The vector explains it: anyone on the network can build the link without an account, and once it is opened the damage lands outside the vulnerable component, in the user's browser, with high impact on both what they can see and what they can do.4
Edge rules bought time. They only cover the default path.
The advisory lists three interim mitigations: block /_serverFn/* requests that lack x-tsr-serverFn: true, filter browser navigations to those paths, and use a restrictive Content Security Policy.4
Appwrite applied the first two for every site on Appwrite Cloud the same day: its edge requires x-tsr-serverfn: true and blocks requests with Sec-Fetch-Mode: navigate or text/html in Accept. Those rules cover only the default server-function path; apps that moved it need their own firewall rules.8
[Edge rules] cannot replace the patch.
Same project, four months apart, opposite advice.
Two TanStack security events landed in 2026, and several package names appear in both. They need different responses, so it is worth keeping them apart.
| Compared | May 2026 · CVE-2026-45321 | September 2026 · CVE-2026-102989 |
|---|---|---|
| Kind | Malicious publish: 84 versions of 42 packages | A bug in code TanStack published honestly |
| Severity | CVSS 9.6 · CWE-506 | CVSS 9.3 · CWE-79 |
| What ran | A credential stealer, on the machine that installed it | An attacker's script, in a victim's browser on your origin |
| Exploited | On CISA KEV since 2026-05-27 | None reported; not on KEV |
| What to do | Rotate every credential the install host could reach | Upgrade, rebuild and redeploy |
Two of the May malicious versions, react-start 1.167.68 and 1.167.71, also fall inside this advisory's range. If you ever installed one, the May advice comes first. Sources: TanStack's May postmortem; GHSA-qx66-fv34-fjm8.3
Only the upgrade closes it. The rest limits what a click can do.
Which controls would have stopped or limited this one. It is a bug in honestly published code, so the controls that guard against malicious packages do not apply.
| Control | Effect here | Why |
|---|---|---|
| Upgrade, rebuild, redeploy | Stops it | The only fix: start-server-core 1.169.39 or later, actually running. Check the resolved core, not just the framework package. |
| Edge rule on /_serverFn/* | Breaks step 6 on the default path | Requiring the x-tsr-serverFn header and blocking navigations stops a clicked link from reaching the endpoint. Custom server-function paths need their own rules, and TanStack says rules cannot replace the patch. |
| Restrictive Content Security Policy | Limits damage | Listed by the advisory as an interim mitigation. A strict policy can stop injected script from running even when the HTML reaches the browser (step 7). |
| HttpOnly session cookies | Limits damage | Keeps the session cookie out of the script's reach. The script can still make authenticated requests as the user from their own browser. |
| An advisory feed (GHSA, Dependabot, OSV) | Finds it sooner | The fix's title never said security; only the advisory did. Automated update pull requests with CI keep the time to patched short on a framework that ships often. |
| Minimum release age, provenance, 2FA, credential rotation | Doesn't apply | These answer malicious publishes and stolen secrets. Here the maintainers published the code honestly, and nothing ran on your servers. |
The server-function transport can pass request payload fields into internal middleware state instead of limiting them to public input fields.
Sources
Primary sources are the maintainer, the advisory database, the registry and government. Secondary sources are other organizations' own analyses.
Primary · maintainer
- 1TanStack (Tanner Linsley)TanStack Start security update: CVE-2026-102989 and its publication PR #1314
CVE and advisory ids, severity, the version table, checking the resolved core, remediation, edge-rule caveat, the Lovable credit.
- 2TanStackFix PR #8573 and commit d521abd71c
Fix timing and the diff behind steps 3, 4 and 8. The header-order detail in step 4 is our reading of this diff, not the advisory's wording.
- 3TanStack (Tanner Linsley)npm supply chain compromise postmortem
The May 2026 incident, for the comparison.
Primary · advisory databases, registry and government
- 4GitHub / TanStackGHSA-qx66-fv34-fjm8
CVSS 9.3 and its vector, CWE-79, affected ranges, the root-cause sentence, the interim mitigations.
- 5npm registrystart-server-core · react-start · solid-start · vue-start
Publish times of the first affected, last vulnerable and patched versions.
- 6NIST and the CVE Program
- 7CISAKnown Exploited Vulnerabilities catalog (feed)
Not listed for this CVE; the May compromise was added on 2026-05-27.
Secondary · hosting provider
- 8AppwriteTanStack Start server-function XSS
The edge rules it deployed for Appwrite Cloud sites, and their limit to the default path.
Written from sources checked on 2026-09-30, the day of disclosure; facts may still develop. Every fact above traces to one of them; nothing unconfirmed is included. Pull quotes are verbatim from the linked primary pages; the bracketed words in TanStack's are ours.