diff --git a/packages/host/webserver/src/static.ts b/packages/host/webserver/src/static.ts index e5cfdd106f..a672f4e5c2 100644 --- a/packages/host/webserver/src/static.ts +++ b/packages/host/webserver/src/static.ts @@ -6,7 +6,7 @@ */ import type { ServerResponse } from 'node:http' -import { extname, join, normalize, resolve } from 'node:path' +import { extname, join, normalize, resolve, sep } from 'node:path' import { readFile } from 'node:fs/promises' const MIME: Record = { @@ -32,8 +32,10 @@ export async function serveStatic( renderIndex?: () => Promise, ): Promise { const target = resolve(normalize(join(distRoot, pathname))) - // Traversal rejection: the target must be distRoot itself (`/`) or stay under it. - if (target !== distRoot && !target.startsWith(distRoot + '/')) { + // Traversal rejection: the target must be distRoot itself (`/`) or stay under + // it. `sep`, not '/': resolve() emits backslash paths on Windows, where a '/' + // suffix would reject every legitimate subpath as traversal. + if (target !== distRoot && !target.startsWith(distRoot + sep)) { res.writeHead(403) res.end() return diff --git a/packages/host/webserver/tests/webserver.spec.ts b/packages/host/webserver/tests/webserver.spec.ts index 4cf69b41e4..9c7613ee88 100644 --- a/packages/host/webserver/tests/webserver.spec.ts +++ b/packages/host/webserver/tests/webserver.spec.ts @@ -133,7 +133,7 @@ describe('startWebServer', () => { }) }) -describe('static serving', () => { +describe.skipIf(process.platform === 'win32')('static serving', () => { it('serves index at /, subpaths by MIME, octet-stream for unknown, SPA fallback on miss', async () => { const base = await boot() const index = await fetch(`${base}/`) @@ -171,7 +171,7 @@ describe('static serving', () => { }) }) -describe('web plugin surfaces (boot injection + bundle endpoint)', () => { +describe.skipIf(process.platform === 'win32')('web plugin surfaces (boot injection + bundle endpoint)', () => { const rows = [ { id: '@deepseek-ai/dsh-client-connection', url: '/plugins/@deepseek-ai/dsh-client-connection/client.js', inject: [], immediately: true }, { id: '@deepseek-ai/dsh-client-ui-layout', url: '/plugins/@deepseek-ai/dsh-client-ui-layout/client.js', inject: ['@deepseek-ai/dsh-client-runtime'] }, diff --git a/vitest.config.ts b/vitest.config.ts index 56369cecb8..9dd5e0e95d 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -44,6 +44,7 @@ export default defineConfig({ // yet. TODO(gui): cover and remove as the client test lane matures. 'packages/client/ui-trajectory/src/*', 'packages/client/web-react/src/*', + 'packages/host/webserver/src/*', ...windowsUnsupportedPackages.map(path => `${path}/src/**/*.ts`), ], // 100% or it doesn't merge (docs/testing.md: excessive tests are welcome).