ApostropheCMS import-export tar guard misses './..' because it is a literal '../' substring test
### Summary The tar-extraction guard added to `@apostrophecms/import-export` for GHSA-mwxc-m426-3f78 is a literal substring test for `../`, and `./..` does not contain that substring. An entry named `./..` therefore passes the guard, `path.join` normalizes it to the parent of the export directory, and the resulting `fs.WriteStream` opens a directory and emits `EISDIR` with no `error` listener attached, which terminates the Node process. Confirmed on default-branch HEAD `1bd5d0a7a6fe880836fa75adad6964f1c577d640`, package version `3.6.2`. ### Details `packages/import-export/lib/formats/gzip.js`, in the tar-stream `entry` handler inside `extract(filepath, exportPath)`: ```js const name = header.name.replace(/\\/g, '/'); if (name.includes('../')) { // skipped } ... stream.pipe(fs.WriteStream(path.join(exportPath, name))); ``` Two separate problems sit in those four lines. The guard tests the raw entry name for a substring instead of resolving the joined path and requiring containment, and the write stream has no `error` handler, so any stream error becomes an unhandled `'error'` event. Reaching it needs an account that can import, nothing more. `packages/import-export/modules/@apostrophecms/import-export-page/index.js` exposes `post.importExportImport`, which calls into `lib/methods/import.js`; that handler requires `req.user` and then calls `format.input(exportPath)`, which for a `.gz` upload is `gzip.js` calling `extract()` on the uploaded archive. ### PoC This is not an end-to
- Products
- @apostrophecms/import-export
- Affected versions
- Not specified
- Fixed versions
- 3.7.0
- CVEs
- CVE-2026-113664
- Confirmed fixed CVEs
- Not specified
- Updated in this feed
- 31 Oct 2026, 13:14 UTC · Revision 1
Source evidence
ApostropheCMS import-export tar guard misses './..' because it is a literal '../' substring test
core-osv · Original sourcePublished 20 Oct 2026, 18:23 UTC
### Summary The tar-extraction guard added to `@apostrophecms/import-export` for GHSA-mwxc-m426-3f78 is a literal substring test for `../`, and `./..` does not contain that substring. An entry named `./..` therefore passes the guard, `path.join` normalizes it to the parent of the export directory, and the resulting `fs.WriteStream` opens a directory and emits `EISDIR` with no `error` listener attached, which terminates the Node process. Confirmed on default-branch HEAD `1bd5d0a7a6fe880836fa75adad6964f1c577d640`, package version `3.6.2`. ### Details `packages/import-export/lib/formats/gzip.js`, in the tar-stream `entry` handler inside `extract(filepath, exportPath)`: ```js const name = header.name.replace(/\\/g, '/'); if (name.includes('../')) { // skipped } ... stream.pipe(fs.WriteStream(path.join(exportPath, name))); ``` Two separate problems sit in those four lines. The guard tests the raw entry name for a substring instead of resolving the joined path and requiring containment, and the write stream has no `error` handler, so any stream error becomes an unhandled `'error'` event. Reaching it needs an account that can import, nothing more. `packages/import-export/modules/@apostrophecms/import-export-page/index.js` exposes `post.importExportImport`, which calls into `lib/methods/import.js`; that handler requires `req.user` and then calls `format.input(exportPath)`, which for a `.gz` upload is `gzip.js` calling `extract()` on the uploaded archive. ### PoC This is not an end-to
osv:GHSA-97wv-p4xx-c7mg
@apostrophecms/import-export
Fixed versions: 3.7.0
Complete source record
{
"id": "a94fe7d214047a7815b56b51a0774dbc891641cd0bc1dbc1a7ee23efd42ec304",
"url": "https://osv.dev/vulnerability/GHSA-97wv-p4xx-c7mg",
"method": "structured",
"payload": {
"kind": "advisory",
"title": "ApostropheCMS import-export tar guard misses './..' because it is a literal '../' substring test",
"summary": "### Summary The tar-extraction guard added to `@apostrophecms/import-export` for GHSA-mwxc-m426-3f78 is a literal substring test for `../`, and `./..` does not contain that substring. An entry named `./..` therefore passes the guard, `path.join` normalizes it to the parent of the export directory, and the resulting `fs.WriteStream` opens a directory and emits `EISDIR` with no `error` listener attached, which terminates the Node process. Confirmed on default-branch HEAD `1bd5d0a7a6fe880836fa75adad6964f1c577d640`, package version `3.6.2`. ### Details `packages/import-export/lib/formats/gzip.js`, in the tar-stream `entry` handler inside `extract(filepath, exportPath)`: ```js const name = header.name.replace(/\\\\/g, '/'); if (name.includes('../')) { // skipped } ... stream.pipe(fs.WriteStream(path.join(exportPath, name))); ``` Two separate problems sit in those four lines. The guard tests the raw entry name for a substring instead of resolving the joined path and requiring containment, and the write stream has no `error` handler, so any stream error becomes an unhandled `'error'` event. Reaching it needs an account that can import, nothing more. `packages/import-export/modules/@apostrophecms/import-export-page/index.js` exposes `post.importExportImport`, which calls into `lib/methods/import.js`; that handler requires `req.user` and then calls `format.input(exportPath)`, which for a `.gz` upload is `gzip.js` calling `extract()` on the uploaded archive. ### PoC This is not an end-to",
"evidence": "osv:GHSA-97wv-p4xx-c7mg",
"products": [
{
"name": {
"value": "@apostrophecms/import-export",
"evidence": "/affected/0/package/name"
},
"prerequisites": [],
"fixed_versions": [
{
"value": "3.7.0",
"evidence": "/affected/0/ranges"
}
],
"affected_versions": []
}
],
"severity": "medium",
"structured": {
"id": "GHSA-97wv-p4xx-c7mg",
"aliases": [
"CVE-2026-113664",
"GHSA-97wv-p4xx-c7mg"
],
"metrics": [
{
"type": "CVSS_V4",
"score": "CVSS:4.0/AV:N/AC:L/AT:N/PR:L/UI:N/VC:L/VI:L/VA:L/SC:N/SI:N/SA:N"
}
],
"affected": [
{
"ranges": [
{
"type": "SEMVER",
"events": [
{
"introduced": "0"
},
{
"fixed": "3.7.0"
}
]
}
],
"package": {
"name": "@apostrophecms/import-export",
"purl": "pkg:npm/%40apostrophecms/import-export",
"ecosystem": "npm"
},
"database_specific": {
"source": "https://github.com/github/advisory-database/blob/main/advisories/github-reviewed/2026/10/GHSA-97wv-p4xx-c7mg/GHSA-97wv-p4xx-c7mg.json"
}
}
],
"modified": "2026-10-20T18:30:04.614026478Z",
"provider": "osv",
"references": [
"https://github.com/apostrophecms/apostrophe/security/advisories/GHSA-97wv-p4xx-c7mg",
"https://github.com/apostrophecms/apostrophe/commit/9500b8b0e1d630d5e07fbb5fd2c25ed0943845ce",
"https://github.com/apostrophecms/apostrophe/commit/f84fd41cbf6c22eb8d0008fc9c25df1f7130b350",
"https://github.com/apostrophecms/apostrophe",
"https://github.com/apostrophecms/apostrophe/releases/tag/@apostrophecms/import-export@3.7.0"
],
"provider_metadata": {
"cwe_ids": [
"CWE-22"
],
"severity": "MODERATE",
"github_reviewed": true,
"nvd_published_at": null,
"github_reviewed_at": "2026-10-20T18:23:42Z"
}
},
"related_cves": [
{
"value": "CVE-2026-113664",
"evidence": "/aliases"
}
],
"security_relevant": true,
"severity_evidence": "/severity/0/score"
},
"source_id": "core-osv",
"document_id": 390152,
"published_at": "2026-10-20T18:23:42+00:00"
}