From 278a39a47b5d5ea0087458d82edf7206faff03ec Mon Sep 17 00:00:00 2001 From: Andy Burke Date: Tue, 12 Aug 2025 15:24:55 -0700 Subject: [PATCH] fix: handle uploads of filenames that require uri encoding --- deno.json | 2 +- handlers/static.ts | 2 +- tests/01_static_files.test.ts | 92 +++++++++++++++++++++++++++++++++++ 3 files changed, 94 insertions(+), 2 deletions(-) diff --git a/deno.json b/deno.json index 695531c..ce885e7 100644 --- a/deno.json +++ b/deno.json @@ -1,7 +1,7 @@ { "name": "@andyburke/serverus", "description": "A flexible HTTP server for mixed content. Throw static files, markdown, Typescript and (hopefully, eventually) more into a directory and serverus can serve it up a bit more like old-school CGI.", - "version": "0.12.3", + "version": "0.12.4", "license": "MIT", "exports": { ".": "./serverus.ts", diff --git a/handlers/static.ts b/handlers/static.ts index bd10e53..72149bf 100644 --- a/handlers/static.ts +++ b/handlers/static.ts @@ -124,7 +124,7 @@ export const HANDLERS: Partial> = { } for await (const file of files) { - const filename: string = file.name ?? path.basename(normalized_path); + const filename: string = file.name ? decodeURIComponent(file.name) : path.basename(normalized_path); const resolved_upload_name = path.resolve(path.join(upload_directory, filename)); if (!resolved_upload_name.startsWith(root)) { diff --git a/tests/01_static_files.test.ts b/tests/01_static_files.test.ts index e0607f2..b673112 100644 --- a/tests/01_static_files.test.ts +++ b/tests/01_static_files.test.ts @@ -275,6 +275,98 @@ Deno.test({ } }); +Deno.test({ + name: 'ensure filenames with HTML escapes work', + permissions: { + env: true, + read: true, + write: true, + net: true + }, + + sanitizeResources: false, + sanitizeOps: false, + + fn: async () => { + let test_server_info: EPHEMERAL_SERVER | null = null; + const cwd = Deno.cwd(); + + const PREVIOUS_PUT_PATHS_ALLOWED = Deno.env.get('SERVERUS_PUT_PATHS_ALLOWED'); + try { + Deno.chdir('./tests/www'); + Deno.env.set('SERVERUS_PUT_PATHS_ALLOWED', path.join(Deno.cwd(), 'files')); + + test_server_info = await get_ephemeral_listen_server(); + + const put_body = new FormData(); + let test_file: File | undefined = new File(['this is a test upload with strange filename'], encodeURIComponent('2Q==(30).txt')); + put_body.append('file', test_file); + + // Sending a single file + const put_response = await fetch(`http://${test_server_info.hostname}:${test_server_info.port}/files/2Q==(30).txt`, { + method: 'PUT', + body: put_body + }); + + asserts.assert(put_response.ok); + + const put_response_body = await put_response.json(); + asserts.assert(put_response_body); + asserts.assert(put_response_body.written); + asserts.assertEquals( + put_response_body.written?.[0], + `http://${test_server_info.hostname}:${test_server_info.port}/files/2Q==(30).txt` + ); + + put_body.delete('file'); + test_file = undefined; + + const get_response = await fetch(`http://${test_server_info.hostname}:${test_server_info.port}/files/2Q==(30).txt`, { + method: 'GET' + }); + + asserts.assert(get_response.ok); + asserts.assert(get_response.body); + + const local_download_path = path.join(Deno.cwd(), 'files', '2Q==(30).txt-downloaded'); + await ensureFile(local_download_path); + + const file = await Deno.open(local_download_path, { truncate: true, write: true }); + await get_response.body.pipeTo(file.writable); + + const download_content = await Deno.readTextFile(local_download_path); + asserts.assert(download_content); + asserts.assertEquals(download_content, 'this is a test upload with strange filename'); + + await Deno.remove(local_download_path); + asserts.assert(!fs.existsSync(local_download_path)); + + const local_upload_path = path.join(Deno.cwd(), 'files', '2Q==(30).txt'); + asserts.assert(fs.existsSync(local_upload_path)); + + const stat = await Deno.lstat(local_upload_path); + asserts.assert(stat); + asserts.assert(stat.isFile); + asserts.assertEquals(stat.size, 43); + asserts.assertEquals(stat.mode! & 0o777, 0o755); + + await Deno.remove(local_upload_path); + asserts.assert(!fs.existsSync(local_upload_path)); + } finally { + Deno.chdir(cwd); + if (PREVIOUS_PUT_PATHS_ALLOWED) { + Deno.env.set('SERVERUS_PUT_PATHS_ALLOWED', PREVIOUS_PUT_PATHS_ALLOWED); + } else { + Deno.env.delete('SERVERUS_PUT_PATHS_ALLOWED'); + } + + if (test_server_info) { + await test_server_info?.server?.stop(); + } + } + } +}); + Deno.test({ name: 'allow PUT to multiple files in a directory', permissions: {