Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 15 additions & 3 deletions src/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -140,11 +140,18 @@ export function createAppEventHandler(stack: Stack, options: AppOptions) {
event.node.req.originalUrl =
event.node.req.originalUrl || event.node.req.url || "/";

// Preserve the raw (percent-encoded) URL for proxies and Node.js middleware
// that expect req.url in its original encoded form (RFC 3986).
const _rawReqUrl = event.node.req.url || "/";

// Decode percent-encoded path segments to prevent auth bypass via encoding tricks.
// Only decode the path portion, not the query string, to avoid double-decoding.
const _reqPath = _decodePath(event._path || event.node.req.url || "/");
const _reqPath = _decodePath(event._path || _rawReqUrl);
event._path = _reqPath;

// Fast path: skip raw tracking when URL had nothing to decode
const _needsRawUrl = _reqPath !== _rawReqUrl;

// Layer path is the path without the prefix
let _layerPath: string;

Expand All @@ -169,9 +176,14 @@ export function createAppEventHandler(stack: Stack, options: AppOptions) {
continue;
}

// 3. Update event path with layer path
// 3. Update event path (decoded for h3 internal routing)
// and req.url (raw encoded for HTTP proxies and Node.js middleware)
event._path = _layerPath;
event.node.req.url = _layerPath;
event.node.req.url = _needsRawUrl
? layer.route.length > 1
? _rawReqUrl.slice(layer.route.length) || "/"
: _rawReqUrl
: _layerPath;

// 4. Handle request
const val = await layer.handler(event);
Expand Down
19 changes: 19 additions & 0 deletions test/security.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -280,6 +280,25 @@ describe("path decoding: no regressions", () => {
await req2.get("/test/%61bc");
expect(capturedOriginalUrl).toBe("/test/%61bc");
});

it("req.url preserves percent-encoded UTF-8 characters", async () => {
const app2 = createApp({ debug: false });
let capturedReqUrl: string | undefined;
app2.use(
eventHandler((event) => {
capturedReqUrl = event.node.req.url;
return { path: event.path, reqUrl: capturedReqUrl };
}),
);
const req2 = supertest(toNodeListener(app2)) as any;
// %C3%A9 is the percent-encoded form of "é" (UTF-8)
const res = await req2.get("/test/caf%C3%A9");
expect(res.status).toBe(200);
// event.path should be decoded (for h3 internal routing)
expect(res.body.path).toBe("/test/café");
// req.url must stay percent-encoded (for HTTP proxies and middleware)
expect(res.body.reqUrl).toBe("/test/caf%C3%A9");
});
});

describe("path decoding with useBase", () => {
Expand Down