Skip to content

Commit 0f9630c

Browse files
boutellManohar2503vansh1011myovchevstuartromanek
authored
Stable reconcilation q2 m2 2026 (#5502)
* Fix asset URLs when a site prefix is configured (#5448) * fix: treat col as a self-closing tag (#5447) * fix: treat col as a self-closing tag * Make dateTime field responsive (css) (#5481) * Fix/from rich text adds metatype (#5488) * fromRichText adds metatype to new widget * change * nodemailer major bump (#5485) * Harden and centralize the cache invalidation (#5493) * fix(sanitize-html): emit transformTags text on empty tags when textFilter is set (#5494) When a transformTags handler adds text to an allowed tag that originally had no text content, the injected text was silently dropped if any textFilter was configured. The onopentag branch that emits frame.innerText was guarded by !options.textFilter, deferring emission to ontext so the filter could run there. For an empty element htmlparser2 never fires ontext, so the text was emitted by neither branch. Emit frame.innerText through options.textFilter here when present (mirroring the discard path), so the transformTags text contract holds for empty tags regardless of whether a textFilter is set. * changeset crediting spokodev for sanitize-html fix (#5498) * Fix shortcut conflicts (#5499) * Fix backspace after slash deleting a rich-text widget * Fix copy/paste widget/text conflicts * Fix astro redirects (#5500) * Merge commit from fork * Merge commit from fork * Merge commit from fork * Merge commit from fork * fix path traversal in import/export * correct credits * additional guards * Merge commit from fork * fix for </textarea/> vulnerability (#5501) * wip * fix for math/svg vulnerabilities --------- Co-authored-by: Jinka Manohar <145598597+Manohar2503@users.noreply.github.com> Co-authored-by: Vansh Parmar <vanshparmar8742@gmail.com> Co-authored-by: Miro Yovchev <2827783+myovchev@users.noreply.github.com> Co-authored-by: Stuart Romanek <stuart@apostrophecms.com> Co-authored-by: spokodev <spoko.dev@gmail.com>
1 parent b3e29f0 commit 0f9630c

52 files changed

Lines changed: 2578 additions & 226 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
---
2+
"apostrophe": patch
3+
"@apostrophecms/vite": patch
4+
---
5+
6+
Fixed the admin UI sometimes serving a stale build after dependencies changed (for example after `npm install` or `npm update`). Apostrophe now detects dependency changes from the content of the lock file rather than its modified time, which could be misleading after a fresh checkout or a restored CI/Docker build cache.
7+
8+
For external build module authors: lock file change detection now happens in the core and is passed to the build module via the `lockChanged` build option. The `apos.asset.getSystemLastChangeMs()` helper is deprecated and the build manifest no longer includes a `ts` timestamp.
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@apostrophecms/apostrophe-astro": patch
3+
---
4+
5+
Query string parameters are no longer lost when a URL with a trailing slash is normalized, so `/articles/?page=2` now renders the same content as `/articles?page=2`. Previously such URLs were redirected to the page URL alone (e.g. `/articles`), losing the query string and showing the first page. Redirects to a different origin are now always passed through to the browser.
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"apostrophe": patch
3+
---
4+
5+
Security fix: completed the fix for CVE-2026-39857 (GHSA-xmpp-f9v3-r7qh). The `.choices()` / `.counts()` query builders (`?choices=` / `?counts=` on the public REST API) guarded against leaking distinct values of fields excluded from `publicApiProjection` by resolving the schema field with an exact-name match. A relationship field registers extra query builders whose names differ from the field name — the "slug" alias builders that drop the leading underscore (`author` / `authorAnd` for a field named `_author`) and the `_authorAnd` operation builder — so those aliases were not gated and could still be used by an unauthenticated caller to extract the relationship's distinct choices (the referenced, publicly visible related documents by title/slug, plus per-value counts via `?counts=`) for a relationship an operator intentionally excluded from `publicApiProjection`. Relationship alias builders are now resolved back to their underlying schema field (matching the field name or its `idsStorage`) before the `publicApiProjection` and `viewPermission` checks are applied, so the alias names are gated exactly like the field itself. Thanks to Ta Duc Thien ([thientd](https://github.com/thientd)) for reporting this issue.

‎.changeset/fresh-breads-burn.md‎

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
---
2+
"sanitize-html": patch
3+
---
4+
5+
Allow transformTags to emit text when textFilter is set, even if the tag
6+
is initially empty. This is consistent with the documentation. Thanks to
7+
[spokodev](https://github.com/spokodev) for the fix.
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@apostrophecms/import-export": patch
3+
---
4+
5+
Security: hardened the importer against MongoDB query-operator smuggling through archive metadata (CWE-943). The `aposDocs.json` / `aposAttachments.json` files inside an uploaded archive are parsed with EJSON, which revives objects such as `{ "$ne": null }` as live values. Several attacker-controlled fields — an attachment's `_id`, and a document's `aposLocale`, `parkedId` and `type` — flowed unlaundered into MongoDB selectors (`attachment.db.findOne({ _id })` and the singleton/parked-document lookups), where an object value could act as a query operator. Imported attachment `_id`, `name` and `extension` must now be plain strings, and the singleton/parked lookup selectors coerce their inputs to strings, so no operator can reach the database. The practical impact was limited (field-level operators only — no top-level `$where`/`$expr`, no authentication or privilege escalation, and the importer already owns the documents), but untrusted archive data should never reach a query unlaundered. Found during an internal security review.
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@apostrophecms/import-export": patch
3+
---
4+
5+
Security: the import routine reconstructed each imported attachment's on-disk source path by concatenating the `_id`, `name` and `extension` fields taken directly from the untrusted `aposAttachments.json` inside an uploaded archive, with no traversal check. The archive's existing zip-slip guard only validates tar entry names during extraction and did not cover this second path, which is built afterward. A `../` sequence (or absolute path) in one of those fields could point the reconstructed path outside the extraction directory, causing an arbitrary host file whose name ends in an allow-listed extension (for example `.txt`, `.csv`, `.pdf`) to be read and copied into the public uploads directory, where it was then served without authentication (CWE-22, GHSA-79qf-vqgc-7xx3). Exploiting this required an authenticated account with the contributor role or higher. Apostrophe now rejects any imported attachment whose reconstructed source path resolves outside the archive's `attachments` directory. Projects using `@apostrophecms/import-export` with untrusted contributors should upgrade promptly. Thanks to [kah-ja](https://github.com/kah-ja) and [luuhung1217](https://github.com/luuhung1217) for responsibly reporting the vulnerability.
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@apostrophecms/import-export": patch
3+
---
4+
5+
Security: hardened archive extraction against a denial-of-service hang (CWE-835). A crafted `.tar.gz` import whose archive contained a *directory* entry with a `../` traversal sequence in its name was correctly rejected by the zip-slip guard, but the extractor never advanced to the next tar entry for directories, so extraction never emitted `finish`, the extraction promise never resolved, and the import request/job hung indefinitely — leaving the uploaded file and a partially-extracted directory on disk. Repeated imports could exhaust connections and disk. The extractor now always drains and advances past a rejected entry regardless of its type. Reaching this requires an authenticated account permitted to import. Found during an internal security review.
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"apostrophe": patch
3+
---
4+
5+
Security: restored destination-parent authorization in the page `move()` operation (GHSA-wr5r-wqp2-x4fh). A regression had gated the destination "create" permission check on the source page being restored out of the archive, which silently disabled that check for every ordinary move. As a result a low-privileged but content-editing user (for example an editor) who could edit at least one page could relocate that page under a parent of a restricted page type they have no create/edit rights over (such as one declaring a higher `editRole`/`publishRole`), and in doing so trigger an unchecked re-ranking of the restricted parent's existing children. A cross-parent move into a non-archive destination now again requires "create" permission on the destination, with the archive-restore path handled as an explicit exception. Thanks to 5ud0 / Tarmo Technologies for reporting the issue.
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"apostrophe": patch
3+
---
4+
5+
Fixed pressing Backspace right after typing `/` in a rich text widget deleting the entire widget. Backspace now removes the slash and closes the insert menu. Global command menu shortcuts also no longer fire for key events already handled and prevented by other UI components.
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
---
2+
"sanitize-html": patch
3+
---
4+
5+
Security: fixed an XSS/allowlist bypass in which the contents of a raw-text
6+
element (`textarea` or `xmp`) nested inside an `svg` or `math` root were
7+
re-emitted without HTML-escaping. `sanitize-html` treated that content as inert
8+
raw text because `htmlparser2` 10.x classified raw-text elements by tag name and
9+
ignored the namespace, but a real HTML5 parser treats `textarea`/`xmp` as
10+
ordinary foreign elements inside SVG/MathML and re-parses their contents as live
11+
markup. As a result, markup and event-handler attributes that the allowlist
12+
never permitted (for example `<svg><textarea><img src=x onerror=alert(1)>`)
13+
could survive sanitization and execute in the browser. This is now fixed on two
14+
fronts: `htmlparser2` was upgraded to 12.x, which is namespace-aware and parses
15+
`textarea`/`xmp` inside SVG/MathML as ordinary elements, so their
16+
non-allowlisted children (such as the injected `img`) are dropped by the
17+
allowlist instead of being preserved as raw text; and any raw-text content
18+
`sanitize-html` still emits for these tags (at HTML integration points such as
19+
`foreignObject`/`mtext`, or outside foreign content) is always HTML-escaped. The
20+
default configuration is not affected; the precondition is an `allowedTags` that
21+
includes `svg` or `math` together with `textarea` or `xmp`. Thanks to
22+
[khoadb175](https://github.com/khoadb175) for responsibly disclosing the
23+
vulnerability.

0 commit comments

Comments
 (0)