From b38388b2742fa3adf6cf51f1bb766441beffffce Mon Sep 17 00:00:00 2001 From: Robin Waslander Date: Tue, 17 Mar 2026 09:49:39 +0100 Subject: [PATCH 1/6] fix: sanitize preview IDs to prevent path traversal and reflected XSS (security) Preview routes accepted unsanitized :id params from the URL, allowing path traversal (../../) to read/write/delete .json files outside the previews directory, and reflected XSS via the 404 page. Added assertSinglePathSegment() validation to all four :id route handlers and escapeHtml() to the 404 response. Both guards already existed in src/utils/security.ts. Bump to v0.62.10. --- CHANGELOG.md | 4 ++++ package.json | 2 +- src/api/routes/previews.ts | 15 ++++++++++----- 3 files changed, 15 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index cd18a76..3f7b730 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,10 @@ All notable changes to Tandem Browser will be documented in this file. +## [v0.62.10] - 2026-03-17 + +- fix: sanitize preview IDs to prevent path traversal and reflected XSS (security) + ## [v0.62.9] - 2026-03-16 - fix: remove all duplicate files with spaces in names from repo diff --git a/package.json b/package.json index d48fc44..4237adc 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "tandem-browser", - "version": "0.62.9", + "version": "0.62.10", "description": "First-party OpenClaw companion browser for human-AI collaboration with built-in security controls", "main": "dist/main.js", "author": "Tandem Browser contributors", diff --git a/src/api/routes/previews.ts b/src/api/routes/previews.ts index e834694..c2f7c8a 100644 --- a/src/api/routes/previews.ts +++ b/src/api/routes/previews.ts @@ -18,6 +18,7 @@ import fs from 'fs'; import path from 'path'; import { tandemDir } from '../../utils/paths'; import { handleRouteError } from '../../utils/errors'; +import { assertSinglePathSegment, escapeHtml } from '../../utils/security'; import type { RouteContext } from '../context'; import { createLogger } from '../../utils/logger'; @@ -188,7 +189,8 @@ export function registerPreviewRoutes(router: Router, ctx: RouteContext): void { // Update an existing preview (triggers live reload in the browser) router.put('/preview/:id', (req: Request, res: Response) => { try { - const id = Array.isArray(req.params.id) ? req.params.id[0] : req.params.id; + const rawId = Array.isArray(req.params.id) ? req.params.id[0] : req.params.id; + const id = assertSinglePathSegment(rawId, 'preview ID'); const existing = readPreview(id); if (!existing) { res.status(404).json({ error: `Preview '${id}' not found` }); @@ -222,7 +224,8 @@ export function registerPreviewRoutes(router: Router, ctx: RouteContext): void { // Serve preview metadata (used by live reload polling) router.get('/preview/:id/meta', (req: Request, res: Response) => { try { - const id = Array.isArray(req.params.id) ? req.params.id[0] : req.params.id; + const rawId = Array.isArray(req.params.id) ? req.params.id[0] : req.params.id; + const id = assertSinglePathSegment(rawId, 'preview ID'); const preview = readPreview(id); if (!preview) { res.status(404).json({ error: `Preview '${id}' not found` }); @@ -238,12 +241,13 @@ export function registerPreviewRoutes(router: Router, ctx: RouteContext): void { // Serve the preview HTML page router.get('/preview/:id', (req: Request, res: Response) => { try { - const id = Array.isArray(req.params.id) ? req.params.id[0] : req.params.id; + const rawId = Array.isArray(req.params.id) ? req.params.id[0] : req.params.id; + const id = assertSinglePathSegment(rawId, 'preview ID'); const preview = readPreview(id); if (!preview) { res.status(404).send(`

Preview not found

-

No preview with id ${id} exists.

+

No preview with id ${escapeHtml(id)} exists.

View all previews

`); return; @@ -259,7 +263,8 @@ export function registerPreviewRoutes(router: Router, ctx: RouteContext): void { // Delete a preview router.delete('/preview/:id', (req: Request, res: Response) => { try { - const id = Array.isArray(req.params.id) ? req.params.id[0] : req.params.id; + const rawId = Array.isArray(req.params.id) ? req.params.id[0] : req.params.id; + const id = assertSinglePathSegment(rawId, 'preview ID'); const p = previewPath(id); if (!fs.existsSync(p)) { res.status(404).json({ error: `Preview '${id}' not found` }); From ac8695ab01b76fdc8196080627876d7046f19e84 Mon Sep 17 00:00:00 2001 From: Robin Waslander Date: Tue, 17 Mar 2026 09:49:39 +0100 Subject: [PATCH 2/6] chore: bump to v0.62.11 --- CHANGELOG.md | 14 ++++++++++++++ package-lock.json | 4 ++-- package.json | 2 +- shell/about.html | 2 +- 4 files changed, 18 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3f7b730..42d9625 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,20 @@ All notable changes to Tandem Browser will be documented in this file. +## [v0.62.11] - 2026-03-17 + +- fix: sanitize preview IDs to prevent path traversal and reflected XSS (security) + +Preview routes accepted unsanitized :id params from the URL, allowing +path traversal (../../) to read/write/delete .json files outside the +previews directory, and reflected XSS via the 404 page. + +Added assertSinglePathSegment() validation to all four :id route handlers +and escapeHtml() to the 404 response. Both guards already existed in +src/utils/security.ts. + +Bump to v0.62.10. + ## [v0.62.10] - 2026-03-17 - fix: sanitize preview IDs to prevent path traversal and reflected XSS (security) diff --git a/package-lock.json b/package-lock.json index a8c772a..390644e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "tandem-browser", - "version": "0.62.9", + "version": "0.62.11", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "tandem-browser", - "version": "0.62.9", + "version": "0.62.11", "hasInstallScript": true, "license": "MIT", "dependencies": { diff --git a/package.json b/package.json index 4237adc..e4a50bf 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "tandem-browser", - "version": "0.62.10", + "version": "0.62.11", "description": "First-party OpenClaw companion browser for human-AI collaboration with built-in security controls", "main": "dist/main.js", "author": "Tandem Browser contributors", diff --git a/shell/about.html b/shell/about.html index bd6951e..6315dcd 100644 --- a/shell/about.html +++ b/shell/about.html @@ -114,7 +114,7 @@
Tandem
First-Party OpenClaw Companion Browser
Developer Preview
-
v0.62.9
+
v0.62.11
Built specifically for human-AI collaboration with OpenClaw.
Maintained in the same ecosystem as OpenClaw, with security and local control built in. From c348338756d6b061e6235f6188a947ba2fabae08 Mon Sep 17 00:00:00 2001 From: Robin Waslander Date: Tue, 17 Mar 2026 09:49:58 +0100 Subject: [PATCH 3/6] chore: clean up changelog duplicate entry --- CHANGELOG.md | 14 -------------- 1 file changed, 14 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 42d9625..88b31c0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,20 +6,6 @@ All notable changes to Tandem Browser will be documented in this file. - fix: sanitize preview IDs to prevent path traversal and reflected XSS (security) -Preview routes accepted unsanitized :id params from the URL, allowing -path traversal (../../) to read/write/delete .json files outside the -previews directory, and reflected XSS via the 404 page. - -Added assertSinglePathSegment() validation to all four :id route handlers -and escapeHtml() to the 404 response. Both guards already existed in -src/utils/security.ts. - -Bump to v0.62.10. - -## [v0.62.10] - 2026-03-17 - -- fix: sanitize preview IDs to prevent path traversal and reflected XSS (security) - ## [v0.62.9] - 2026-03-16 - fix: remove all duplicate files with spaces in names from repo From 49754bfae69e2102410c92cc7732a57cc755bdb2 Mon Sep 17 00:00:00 2001 From: Robin Waslander Date: Tue, 17 Mar 2026 10:48:06 +0100 Subject: [PATCH 4/6] fix: use resolvePathWithinRoot in previewPath to satisfy CodeQL assertSinglePathSegment already blocks traversal, but CodeQL cannot trace that validation. Adding resolvePathWithinRoot as a second guard in previewPath() itself makes the path restriction visible to static analysis. --- src/api/routes/previews.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/api/routes/previews.ts b/src/api/routes/previews.ts index c2f7c8a..c65dd18 100644 --- a/src/api/routes/previews.ts +++ b/src/api/routes/previews.ts @@ -18,7 +18,7 @@ import fs from 'fs'; import path from 'path'; import { tandemDir } from '../../utils/paths'; import { handleRouteError } from '../../utils/errors'; -import { assertSinglePathSegment, escapeHtml } from '../../utils/security'; +import { assertSinglePathSegment, escapeHtml, resolvePathWithinRoot } from '../../utils/security'; import type { RouteContext } from '../context'; import { createLogger } from '../../utils/logger'; @@ -33,7 +33,7 @@ function previewsDir(): string { } function previewPath(id: string): string { - return path.join(previewsDir(), `${id}.json`); + return resolvePathWithinRoot(previewsDir(), `${id}.json`); } function slugify(title: string): string { From c4e5eb6c9bc13891f28b2fe8fe1ed4562570dd01 Mon Sep 17 00:00:00 2001 From: Robin Waslander Date: Tue, 17 Mar 2026 10:48:06 +0100 Subject: [PATCH 5/6] chore: bump to v0.62.12 --- CHANGELOG.md | 9 +++++++++ package-lock.json | 4 ++-- package.json | 2 +- shell/about.html | 2 +- 4 files changed, 13 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 88b31c0..289a861 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,15 @@ All notable changes to Tandem Browser will be documented in this file. +## [v0.62.12] - 2026-03-17 + +- fix: use resolvePathWithinRoot in previewPath to satisfy CodeQL + +assertSinglePathSegment already blocks traversal, but CodeQL cannot +trace that validation. Adding resolvePathWithinRoot as a second guard +in previewPath() itself makes the path restriction visible to static +analysis. + ## [v0.62.11] - 2026-03-17 - fix: sanitize preview IDs to prevent path traversal and reflected XSS (security) diff --git a/package-lock.json b/package-lock.json index 390644e..0c2ccbe 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "tandem-browser", - "version": "0.62.11", + "version": "0.62.12", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "tandem-browser", - "version": "0.62.11", + "version": "0.62.12", "hasInstallScript": true, "license": "MIT", "dependencies": { diff --git a/package.json b/package.json index e4a50bf..7041161 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "tandem-browser", - "version": "0.62.11", + "version": "0.62.12", "description": "First-party OpenClaw companion browser for human-AI collaboration with built-in security controls", "main": "dist/main.js", "author": "Tandem Browser contributors", diff --git a/shell/about.html b/shell/about.html index 6315dcd..f9e3824 100644 --- a/shell/about.html +++ b/shell/about.html @@ -114,7 +114,7 @@
Tandem
First-Party OpenClaw Companion Browser
Developer Preview
-
v0.62.11
+
v0.62.12
Built specifically for human-AI collaboration with OpenClaw.
Maintained in the same ecosystem as OpenClaw, with security and local control built in. From 3ef66462a62fcc36aed149ed11830666ec2534e6 Mon Sep 17 00:00:00 2001 From: Robin Waslander Date: Tue, 17 Mar 2026 10:48:24 +0100 Subject: [PATCH 6/6] chore: consolidate changelog entries for security fix --- CHANGELOG.md | 9 --------- 1 file changed, 9 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 289a861..5af3875 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,15 +4,6 @@ All notable changes to Tandem Browser will be documented in this file. ## [v0.62.12] - 2026-03-17 -- fix: use resolvePathWithinRoot in previewPath to satisfy CodeQL - -assertSinglePathSegment already blocks traversal, but CodeQL cannot -trace that validation. Adding resolvePathWithinRoot as a second guard -in previewPath() itself makes the path restriction visible to static -analysis. - -## [v0.62.11] - 2026-03-17 - - fix: sanitize preview IDs to prevent path traversal and reflected XSS (security) ## [v0.62.9] - 2026-03-16