diff --git a/lib/extension/frontend.ts b/lib/extension/frontend.ts index 257a8ace8c..676dd4290c 100644 --- a/lib/extension/frontend.ts +++ b/lib/extension/frontend.ts @@ -17,6 +17,14 @@ import * as settings from "../util/settings"; import utils from "../util/utils"; import Extension from "./extension"; +function sendBadRequest(response: ServerResponse, error: unknown): void { + const message = error instanceof Error ? error.message : "Bad Request"; + + response.statusCode = 400; + response.setHeader("Content-Type", "text/plain; charset=utf-8"); + response.end(message); +} + /** * This extension servers the frontend */ @@ -99,6 +107,14 @@ export class Frontend extends Extension { request.url = `/${newUrl}`; request.path = request.url; + try { + decodeURIComponent(request.path); + } catch (error) { + sendBadRequest(response, error); + + return; + } + if (newUrl.startsWith("device_icons/")) { request.path = request.path.replace("device_icons/", ""); request.url = request.url.replace("/device_icons", ""); diff --git a/test/extensions/frontend.test.ts b/test/extensions/frontend.test.ts index 4f061ac283..fb54f07580 100644 --- a/test/extensions/frontend.test.ts +++ b/test/extensions/frontend.test.ts @@ -361,6 +361,23 @@ describe("Extension: Frontend", () => { ); }); + it("returns 400 for malformed encoded paths", async () => { + controller = new Controller(vi.fn(), vi.fn()); + await controller.start(); + + const response = { + statusCode: 200, + setHeader: vi.fn(), + end: vi.fn(), + }; + + mockHTTPOnRequest({url: "/%E0%A4%A"}, response); + expect(response.statusCode).toBe(400); + expect(response.setHeader).toHaveBeenCalledWith("Content-Type", "text/plain; charset=utf-8"); + expect(response.end).toHaveBeenCalledTimes(1); + expect(mockNodeStatic[frontendPath]).not.toHaveBeenCalled(); + }); + it("Static server", async () => { controller = new Controller(vi.fn(), vi.fn()); await controller.start();