mirror of
https://github.com/nestjs/nest.git
synced 2026-10-28 17:56:01 +08:00
fix(platform-fastify): absolute-form request target middleware bypass
Requests using an absolute-form request target ("GET http://host/path")
were routed to the handler by Fastify while path-scoped middleware, which
matched against the raw request target, never ran.
- replace the inlined @fastify/middie fork with the upstream package
(9.3.4), which resolves absolute-form targets before matching and
already contains the decoded-path fix the fork was carrying
- resolve absolute-form targets in the adapter's own route check
- fold the deprecated top-level Fastify router options into
"routerOptions" so that @fastify/middie normalizes request paths the
same way the router does
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
1 parent
26e62a9bc3
commit
dda252090e
7 files changed
+162
-441
No files matched your search
@@ -18,6 +18,7 @@ import {
|
||||
} from '@nestjs/platform-fastify';
|
||||
import { Test } from '@nestjs/testing';
|
||||
import { FastifyRequest } from 'fastify';
|
||||
import { AddressInfo, connect } from 'net';
|
||||
import request from 'supertest';
|
||||
import { AppModule } from '../src/app.module.js';
|
||||
|
||||
@@ -1069,4 +1070,98 @@ describe('Middleware (FastifyAdapter)', () => {
|
||||
await app.close();
|
||||
});
|
||||
});
|
||||
|
||||
describe('absolute-form request target', () => {
|
||||
const MIDDLEWARE_RETURN_VALUE = 'middleware_return';
|
||||
const HANDLER_RETURN_VALUE = 'handler_return';
|
||||
|
||||
@Controller('users')
|
||||
class UsersController {
|
||||
@Get()
|
||||
findAll() {
|
||||
return HANDLER_RETURN_VALUE;
|
||||
}
|
||||
}
|
||||
|
||||
@Module({
|
||||
controllers: [UsersController],
|
||||
})
|
||||
class AbsoluteFormModule implements NestModule {
|
||||
configure(consumer: MiddlewareConsumer) {
|
||||
consumer
|
||||
.apply((req, res, next) => res.end(MIDDLEWARE_RETURN_VALUE))
|
||||
.forRoutes({ path: 'users', method: RequestMethod.GET });
|
||||
}
|
||||
}
|
||||
|
||||
let port: number;
|
||||
|
||||
/**
|
||||
* "supertest" and "light-my-request" always send origin-form request
|
||||
* targets, so the request has to be written to the socket manually.
|
||||
*/
|
||||
const sendRawRequest = (requestTarget: string) =>
|
||||
new Promise<{ statusCode: number; body: string }>((resolve, reject) => {
|
||||
let response = '';
|
||||
const socket = connect(port, '127.0.0.1', () => {
|
||||
socket.write(
|
||||
`GET ${requestTarget} HTTP/1.1\r\n` +
|
||||
`Host: 127.0.0.1:${port}\r\n` +
|
||||
`Connection: close\r\n\r\n`,
|
||||
);
|
||||
});
|
||||
socket.setEncoding('utf8');
|
||||
socket.on('data', chunk => (response += chunk));
|
||||
socket.on('error', reject);
|
||||
socket.on('end', () => {
|
||||
const [head, body = ''] = response.split('\r\n\r\n');
|
||||
resolve({ statusCode: Number(head.split(' ')[1]), body });
|
||||
});
|
||||
});
|
||||
|
||||
const createApp = async (adapter: FastifyAdapter) => {
|
||||
app = (
|
||||
await Test.createTestingModule({
|
||||
imports: [AbsoluteFormModule],
|
||||
}).compile()
|
||||
).createNestApplication<NestFastifyApplication>(adapter);
|
||||
|
||||
await app.listen(0, '127.0.0.1');
|
||||
port = (app.getHttpServer().address() as AddressInfo).port;
|
||||
};
|
||||
|
||||
afterEach(async () => {
|
||||
await app.close();
|
||||
});
|
||||
|
||||
it('runs middleware for an origin-form request target', async () => {
|
||||
await createApp(new FastifyAdapter());
|
||||
|
||||
const response = await sendRawRequest('/users');
|
||||
expect(response.statusCode).toBe(200);
|
||||
expect(response.body).toBe(MIDDLEWARE_RETURN_VALUE);
|
||||
});
|
||||
|
||||
it('does not bypass middleware with an absolute-form request target', async () => {
|
||||
await createApp(new FastifyAdapter());
|
||||
|
||||
const response = await sendRawRequest(`http://127.0.0.1:${port}/users`);
|
||||
expect(response.statusCode).toBe(200);
|
||||
expect(response.body).toBe(MIDDLEWARE_RETURN_VALUE);
|
||||
});
|
||||
|
||||
it('does not bypass middleware with an absolute-form request target and router normalization options', async () => {
|
||||
await createApp(
|
||||
new FastifyAdapter({
|
||||
ignoreDuplicateSlashes: true,
|
||||
ignoreTrailingSlash: true,
|
||||
caseSensitive: false,
|
||||
}),
|
||||
);
|
||||
|
||||
const response = await sendRawRequest(`HTTP://127.0.0.1:${port}//USERS/`);
|
||||
expect(response.statusCode).toBe(200);
|
||||
expect(response.body).toBe(MIDDLEWARE_RETURN_VALUE);
|
||||
});
|
||||
});
|
||||
});
|
||||
Generated
+1
-4
@@ -99,7 +99,6 @@
|
||||
"oxlint": "1.83.0",
|
||||
"prettier": "^3.7.4",
|
||||
"redis": "6.2.1",
|
||||
"reusify": "1.1.0",
|
||||
"rxjs-compat": "6.6.7",
|
||||
"socket.io-client": "4.8.3",
|
||||
"supertest": "7.2.2",
|
||||
@@ -1955,7 +1954,6 @@
|
||||
"version": "9.3.4",
|
||||
"resolved": "https://registry.npmjs.org/@fastify/middie/-/middie-9.3.4.tgz",
|
||||
"integrity": "sha512-dd8phTnVWzx3iKPBKSvtRUT8+Igvy/MP+P5HcY2tjejEyeVbyjkD4HgzKessUObue5RRM5J6PZdxtAqgbn84yw==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
@@ -22700,13 +22698,12 @@
|
||||
"dependencies": {
|
||||
"@fastify/cors": "11.3.0",
|
||||
"@fastify/formbody": "9.0.0",
|
||||
"@fastify/middie": "9.3.4",
|
||||
"fast-querystring": "1.1.2",
|
||||
"fastify": "5.12.4",
|
||||
"fastify-plugin": "6.0.0",
|
||||
"find-my-way": "9.9.0",
|
||||
"light-my-request": "6.6.0",
|
||||
"path-to-regexp": "8.4.2",
|
||||
"reusify": "1.1.0",
|
||||
"tslib": "2.8.1"
|
||||
},
|
||||
"funding": {
|
||||
|
||||
@@ -146,7 +146,6 @@
|
||||
"oxlint": "1.83.0",
|
||||
"prettier": "^3.7.4",
|
||||
"redis": "6.2.1",
|
||||
"reusify": "1.1.0",
|
||||
"rxjs-compat": "6.6.7",
|
||||
"socket.io-client": "4.8.3",
|
||||
"supertest": "7.2.2",
|
||||
|
||||
@@ -46,6 +46,7 @@ import {
|
||||
Response as LightMyRequestResponse,
|
||||
} from 'light-my-request';
|
||||
import { pathToRegexp } from 'path-to-regexp';
|
||||
import middie from '@fastify/middie';
|
||||
import {
|
||||
type VersionValue,
|
||||
loadPackage,
|
||||
@@ -69,7 +70,6 @@ import {
|
||||
FastifyViewOptions,
|
||||
} from '../interfaces/external/index.js';
|
||||
import { NestFastifyBodyParserOptions } from '../interfaces/index.js';
|
||||
import middie from './middie/fastify-middie.js';
|
||||
const { safeDecodeURI } = urlSanitizer;
|
||||
|
||||
type FastifyAdapterBaseOptions<
|
||||
@@ -260,6 +260,9 @@ export class FastifyAdapter<
|
||||
: fastify({
|
||||
...(instanceOrOptions as FastifyServerOptions),
|
||||
routerOptions: {
|
||||
...this.getTopLevelRouterOptions(
|
||||
instanceOrOptions as FastifyServerOptions,
|
||||
),
|
||||
...(instanceOrOptions as FastifyServerOptions)?.routerOptions,
|
||||
constraints: {
|
||||
version: this.versionConstraint as any,
|
||||
@@ -932,11 +935,45 @@ export class FastifyAdapter<
|
||||
return this.instance.route(routeToInject);
|
||||
}
|
||||
|
||||
/**
|
||||
* Fastify still accepts the router options ("ignoreTrailingSlash",
|
||||
* "caseSensitive", ...) at the top level, but "initialConfig.routerOptions"
|
||||
* only reflects them when they are passed through "routerOptions". As the
|
||||
* adapter always passes "routerOptions" (for the version constraint), the
|
||||
* top-level values are folded in so that plugins relying on
|
||||
* "initialConfig.routerOptions" (like @fastify/middie) normalize request
|
||||
* paths exactly like the router does.
|
||||
*/
|
||||
private getTopLevelRouterOptions(
|
||||
options?: FastifyServerOptions,
|
||||
): NonNullable<FastifyServerOptions['routerOptions']> {
|
||||
const routerOptions: Record<string, unknown> = {};
|
||||
const routerOptionKeys = [
|
||||
'ignoreTrailingSlash',
|
||||
'ignoreDuplicateSlashes',
|
||||
'caseSensitive',
|
||||
'useSemicolonDelimiter',
|
||||
'maxParamLength',
|
||||
'allowUnsafeRegex',
|
||||
] as const;
|
||||
for (const key of routerOptionKeys) {
|
||||
if (options?.[key] !== undefined) {
|
||||
routerOptions[key] = options[key];
|
||||
}
|
||||
}
|
||||
return routerOptions;
|
||||
}
|
||||
|
||||
private sanitizeUrl(url: string): string {
|
||||
const initialConfig = this.instance.initialConfig as FastifyServerOptions;
|
||||
const routerOptions =
|
||||
initialConfig.routerOptions as Partial<FastifyServerOptions>;
|
||||
|
||||
// Absolute-form request targets ("GET http://host/path HTTP/1.1") must be
|
||||
// resolved to their path before any other normalization, as the Fastify
|
||||
// router does, so that middleware and routes always match the same path.
|
||||
url = this.getPathFromRequestTarget(url);
|
||||
|
||||
if (
|
||||
routerOptions.ignoreDuplicateSlashes ||
|
||||
initialConfig.ignoreDuplicateSlashes
|
||||
@@ -977,4 +1014,30 @@ export class FastifyAdapter<
|
||||
}
|
||||
return path;
|
||||
}
|
||||
|
||||
/**
|
||||
* Mirrors the absolute-form request target handling of "find-my-way".
|
||||
* Returns the path of an absolute-form target ("http://host/path" -> "/path")
|
||||
* and leaves any other request target untouched.
|
||||
*/
|
||||
private getPathFromRequestTarget(url: string): string {
|
||||
if (url.charCodeAt(0) === 47 /* '/' */) {
|
||||
return url;
|
||||
}
|
||||
const schemeEnd = url.indexOf('://');
|
||||
if (schemeEnd === -1) {
|
||||
return url;
|
||||
}
|
||||
const scheme = url.slice(0, schemeEnd).toLowerCase();
|
||||
if (scheme !== 'http' && scheme !== 'https') {
|
||||
return url;
|
||||
}
|
||||
const authorityStart = schemeEnd + 3;
|
||||
const pathStart = url.indexOf('/', authorityStart);
|
||||
if (pathStart === authorityStart || !URL.canParse(url)) {
|
||||
// Malformed target: the router rejects it before any middleware runs
|
||||
return url;
|
||||
}
|
||||
return pathStart === -1 ? '/' : url.slice(pathStart);
|
||||
}
|
||||
}
|
||||
@@ -1,432 +0,0 @@
|
||||
/* eslint-disable @typescript-eslint/no-this-alias */
|
||||
/* eslint-disable @typescript-eslint/no-namespace */
|
||||
import {
|
||||
FastifyInstance,
|
||||
FastifyPluginCallback,
|
||||
FastifyReply,
|
||||
FastifyRequest,
|
||||
FastifyServerOptions,
|
||||
HookHandlerDoneFunction,
|
||||
} from 'fastify';
|
||||
import fp from 'fastify-plugin';
|
||||
import urlSanitizer from 'find-my-way/lib/url-sanitizer.js';
|
||||
import * as http from 'node:http';
|
||||
import { Path, pathToRegexp } from 'path-to-regexp';
|
||||
import reusify from 'reusify';
|
||||
const { safeDecodeURI } = urlSanitizer;
|
||||
|
||||
export type MiddlewareFn<
|
||||
Req extends { url: string; originalUrl?: string },
|
||||
Res extends { finished?: boolean; writableEnded?: boolean },
|
||||
Ctx = unknown,
|
||||
> = (req: Req, res: Res, next: (err?: unknown) => void) => void;
|
||||
|
||||
interface MiddlewareEntry<
|
||||
Req extends { url: string; originalUrl?: string },
|
||||
Res extends { finished?: boolean; writableEnded?: boolean },
|
||||
Ctx,
|
||||
> {
|
||||
regexp?: RegExp;
|
||||
fn: MiddlewareFn<Req, Res, Ctx>;
|
||||
}
|
||||
|
||||
function bindLast<F extends (...args: any[]) => any>(
|
||||
fn: F,
|
||||
last: Last<Parameters<F>>,
|
||||
): (...args: DropLast<Parameters<F>>) => ReturnType<F> {
|
||||
return (...args: any[]) => fn(...args, last);
|
||||
}
|
||||
|
||||
// Helper types
|
||||
type Last<T extends any[]> = T extends [...any[], infer L] ? L : never;
|
||||
type DropLast<T extends any[]> = T extends [...infer Rest, any] ? Rest : never;
|
||||
|
||||
/**
|
||||
* A clone of `@fastify/middie` engine https://github.com/fastify/middie
|
||||
* with an extra vulnerability fix. Path is now decoded before matching to
|
||||
* avoid bypassing middleware with encoded characters.
|
||||
*/
|
||||
function middie<
|
||||
Req extends { url: string; originalUrl?: string },
|
||||
Res extends { finished?: boolean; writableEnded?: boolean },
|
||||
Ctx = unknown,
|
||||
>(
|
||||
complete: (err: unknown, req: Req, res: Res, ctx: Ctx) => void,
|
||||
initialConfig: FastifyServerOptions | null,
|
||||
) {
|
||||
const middlewares: MiddlewareEntry<Req, Res, Ctx>[] = [];
|
||||
const pool = reusify(Holder as any);
|
||||
|
||||
return {
|
||||
use,
|
||||
run: bindLast(run, initialConfig),
|
||||
};
|
||||
|
||||
function use(
|
||||
this: unknown,
|
||||
url:
|
||||
| string
|
||||
| null
|
||||
| MiddlewareFn<Req, Res, Ctx>
|
||||
| MiddlewareFn<Req, Res, Ctx>[],
|
||||
f?: MiddlewareFn<Req, Res, Ctx> | MiddlewareFn<Req, Res, Ctx>[],
|
||||
) {
|
||||
if (f === undefined) {
|
||||
f = url as MiddlewareFn<Req, Res, Ctx> | MiddlewareFn<Req, Res, Ctx>[];
|
||||
url = null;
|
||||
}
|
||||
|
||||
let regexp: RegExp | undefined;
|
||||
if (typeof url === 'string') {
|
||||
const pathRegExp = pathToRegexp(sanitizePrefixUrl(url) as Path, {
|
||||
end: false,
|
||||
});
|
||||
regexp = pathRegExp.regexp;
|
||||
}
|
||||
|
||||
if (Array.isArray(f)) {
|
||||
for (const val of f) {
|
||||
middlewares.push({ regexp, fn: val });
|
||||
}
|
||||
} else {
|
||||
middlewares.push({ regexp, fn: f });
|
||||
}
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
function run(
|
||||
req: Req,
|
||||
res: Res,
|
||||
ctx: Ctx,
|
||||
initialConfig: FastifyServerOptions | null,
|
||||
) {
|
||||
if (!middlewares.length) {
|
||||
complete(null, req, res, ctx);
|
||||
return;
|
||||
}
|
||||
|
||||
req.originalUrl = req.url;
|
||||
|
||||
const holder = pool.get() as any as HolderInstance;
|
||||
holder.req = req;
|
||||
holder.res = res;
|
||||
holder.url = sanitizeUrl(req.url);
|
||||
holder.context = ctx;
|
||||
holder.initialConfig = initialConfig;
|
||||
holder.done();
|
||||
}
|
||||
|
||||
interface HolderInstance {
|
||||
req: Req | null;
|
||||
res: Res | null;
|
||||
url: string | null;
|
||||
context: Ctx | null;
|
||||
initialConfig: FastifyServerOptions | null;
|
||||
i: number;
|
||||
done: (err?: unknown) => void;
|
||||
}
|
||||
|
||||
function Holder(this: HolderInstance) {
|
||||
this.req = null;
|
||||
this.res = null;
|
||||
this.url = null;
|
||||
this.context = null;
|
||||
this.initialConfig = null;
|
||||
this.i = 0;
|
||||
|
||||
const that = this;
|
||||
|
||||
this.done = function (err?: unknown) {
|
||||
const req = that.req!;
|
||||
const res = that.res!;
|
||||
const url = that.url!;
|
||||
const context = that.context!;
|
||||
const i = that.i++;
|
||||
|
||||
req.url = req.originalUrl!;
|
||||
|
||||
if (res.finished === true || res.writableEnded === true) {
|
||||
cleanup();
|
||||
return;
|
||||
}
|
||||
|
||||
if (err || middlewares.length === i) {
|
||||
complete(err, req, res, context);
|
||||
cleanup();
|
||||
} else {
|
||||
const { fn, regexp } = middlewares[i];
|
||||
|
||||
if (regexp) {
|
||||
// Decode URL before matching to avoid bypassing middleware
|
||||
let sanitizedUrl = url;
|
||||
if (
|
||||
that.initialConfig!.ignoreDuplicateSlashes ||
|
||||
that.initialConfig!.routerOptions?.ignoreDuplicateSlashes
|
||||
) {
|
||||
sanitizedUrl = removeDuplicateSlashes(sanitizedUrl);
|
||||
}
|
||||
|
||||
if (
|
||||
that.initialConfig!.ignoreTrailingSlash ||
|
||||
that.initialConfig!.routerOptions?.ignoreTrailingSlash
|
||||
) {
|
||||
sanitizedUrl = trimLastSlash(sanitizedUrl);
|
||||
}
|
||||
|
||||
if (
|
||||
that.initialConfig!.caseSensitive === false ||
|
||||
that.initialConfig!.routerOptions?.caseSensitive === false
|
||||
) {
|
||||
sanitizedUrl = sanitizedUrl.toLowerCase();
|
||||
}
|
||||
|
||||
const decodedUrl = safeDecodeURI(
|
||||
sanitizedUrl,
|
||||
(that.initialConfig?.routerOptions as any)?.useSemicolonDelimiter ||
|
||||
that.initialConfig?.useSemicolonDelimiter,
|
||||
).path;
|
||||
const result = regexp.exec(decodedUrl);
|
||||
if (result) {
|
||||
req.url = req.url.replace(result[0], '');
|
||||
if (req.url[0] !== '/') req.url = '/' + req.url;
|
||||
fn(req, res, that.done);
|
||||
} else {
|
||||
that.done();
|
||||
}
|
||||
} else {
|
||||
fn(req, res, that.done);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
function cleanup() {
|
||||
that.req = null;
|
||||
that.res = null;
|
||||
that.context = null;
|
||||
that.initialConfig = null;
|
||||
that.i = 0;
|
||||
pool.release(that as any);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function removeDuplicateSlashes(path: string) {
|
||||
const REMOVE_DUPLICATE_SLASHES_REGEXP = /\/\/+/g;
|
||||
return path.indexOf('//') !== -1
|
||||
? path.replace(REMOVE_DUPLICATE_SLASHES_REGEXP, '/')
|
||||
: path;
|
||||
}
|
||||
|
||||
function trimLastSlash(path: string) {
|
||||
if (path.length > 1 && path.charCodeAt(path.length - 1) === 47) {
|
||||
return path.slice(0, -1);
|
||||
}
|
||||
return path;
|
||||
}
|
||||
|
||||
function sanitizeUrl(url: string): string {
|
||||
for (let i = 0, len = url.length; i < len; i++) {
|
||||
const charCode = url.charCodeAt(i);
|
||||
if (charCode === 63 || charCode === 35) {
|
||||
return url.slice(0, i);
|
||||
}
|
||||
}
|
||||
return url;
|
||||
}
|
||||
|
||||
function sanitizePrefixUrl(url: string): string {
|
||||
if (url === '/') return '';
|
||||
if (url[url.length - 1] === '/') return url.slice(0, -1);
|
||||
return url;
|
||||
}
|
||||
|
||||
const kMiddlewares = Symbol('fastify-middie-middlewares');
|
||||
const kMiddie = Symbol('fastify-middie-instance');
|
||||
const kMiddieHasMiddlewares = Symbol('fastify-middie-has-middlewares');
|
||||
|
||||
const supportedHooksWithPayload = [
|
||||
'onError',
|
||||
'onSend',
|
||||
'preParsing',
|
||||
'preSerialization',
|
||||
] as const;
|
||||
|
||||
const supportedHooksWithoutPayload = [
|
||||
'onRequest',
|
||||
'onResponse',
|
||||
'onTimeout',
|
||||
'preHandler',
|
||||
'preValidation',
|
||||
] as const;
|
||||
|
||||
const supportedHooks = [
|
||||
...supportedHooksWithPayload,
|
||||
...supportedHooksWithoutPayload,
|
||||
] as const;
|
||||
|
||||
type SupportedHook = (typeof supportedHooks)[number];
|
||||
|
||||
interface MiddieOptions {
|
||||
hook?: SupportedHook;
|
||||
}
|
||||
|
||||
function fastifyMiddie(
|
||||
fastify: FastifyInstance,
|
||||
options: MiddieOptions,
|
||||
next: (err?: Error) => void,
|
||||
) {
|
||||
fastify.decorate('use', use as any);
|
||||
fastify[kMiddlewares] = [];
|
||||
fastify[kMiddieHasMiddlewares] = false;
|
||||
fastify[kMiddie] = middie(onMiddieEnd, fastify.initialConfig);
|
||||
|
||||
const hook = options.hook || 'onRequest';
|
||||
|
||||
if (!supportedHooks.includes(hook)) {
|
||||
next(new Error(`The hook "${hook}" is not supported by fastify-middie`));
|
||||
return;
|
||||
}
|
||||
|
||||
fastify
|
||||
.addHook(
|
||||
hook,
|
||||
supportedHooksWithPayload.includes(hook as any)
|
||||
? runMiddieWithPayload
|
||||
: runMiddie,
|
||||
)
|
||||
.addHook('onRegister', onRegister);
|
||||
|
||||
function use(this: FastifyInstance, path: string | null, fn?: Function) {
|
||||
if (typeof path === 'string') {
|
||||
const prefix = this.prefix;
|
||||
path = prefix + (path === '/' && prefix.length > 0 ? '' : path);
|
||||
}
|
||||
|
||||
this[kMiddlewares].push([path, fn]);
|
||||
|
||||
if (fn == null) {
|
||||
this[kMiddie].use(path);
|
||||
} else {
|
||||
this[kMiddie].use(path, fn);
|
||||
}
|
||||
|
||||
this[kMiddieHasMiddlewares] = true;
|
||||
return this;
|
||||
}
|
||||
|
||||
function runMiddie(
|
||||
this: FastifyInstance,
|
||||
req: FastifyRequest,
|
||||
reply: FastifyReply,
|
||||
next: HookHandlerDoneFunction,
|
||||
) {
|
||||
if (this[kMiddieHasMiddlewares]) {
|
||||
const raw = req.raw as any;
|
||||
raw.id = req.id;
|
||||
raw.hostname = req.hostname;
|
||||
raw.protocol = req.protocol;
|
||||
raw.ip = req.ip;
|
||||
raw.ips = req.ips;
|
||||
raw.log = req.log;
|
||||
(req.raw as any).query = req.query;
|
||||
(reply.raw as any).log = req.log;
|
||||
if (req.body !== undefined) (req.raw as any).body = req.body;
|
||||
this[kMiddie].run(req.raw, reply.raw, next);
|
||||
} else {
|
||||
next();
|
||||
}
|
||||
}
|
||||
|
||||
function runMiddieWithPayload(
|
||||
this: FastifyInstance,
|
||||
req: FastifyRequest,
|
||||
reply: FastifyReply,
|
||||
_payload: unknown,
|
||||
next: HookHandlerDoneFunction,
|
||||
) {
|
||||
runMiddie.bind(this)(req, reply, next);
|
||||
}
|
||||
|
||||
function onMiddieEnd(
|
||||
err: unknown,
|
||||
_req: any,
|
||||
_res: any,
|
||||
next: (err?: unknown) => void,
|
||||
) {
|
||||
next(err);
|
||||
}
|
||||
|
||||
function onRegister(instance: FastifyInstance) {
|
||||
const middlewares = instance[kMiddlewares].slice() as Array<Array<unknown>>;
|
||||
instance[kMiddlewares] = [];
|
||||
instance[kMiddie] = middie(onMiddieEnd, instance.initialConfig);
|
||||
instance[kMiddieHasMiddlewares] = false;
|
||||
instance.decorate('use', use as any);
|
||||
for (const middleware of middlewares) {
|
||||
instance[kMiddlewares].push(middleware as any);
|
||||
(instance[kMiddie].use as any)(...middleware);
|
||||
}
|
||||
instance[kMiddieHasMiddlewares] = middlewares.length > 0;
|
||||
}
|
||||
|
||||
next();
|
||||
}
|
||||
|
||||
declare namespace fastifyMiddie {
|
||||
export interface FastifyMiddieOptions {
|
||||
hook?:
|
||||
| 'onRequest'
|
||||
| 'preParsing'
|
||||
| 'preValidation'
|
||||
| 'preHandler'
|
||||
| 'preSerialization'
|
||||
| 'onSend'
|
||||
| 'onResponse'
|
||||
| 'onTimeout'
|
||||
| 'onError';
|
||||
}
|
||||
|
||||
type FastifyMiddie =
|
||||
FastifyPluginCallback<fastifyMiddie.FastifyMiddieOptions>;
|
||||
|
||||
export interface IncomingMessageExtended {
|
||||
body?: any;
|
||||
query?: any;
|
||||
}
|
||||
export type NextFunction = (err?: any) => void;
|
||||
export type SimpleHandleFunction = (
|
||||
req: http.IncomingMessage & IncomingMessageExtended,
|
||||
res: http.ServerResponse,
|
||||
) => void;
|
||||
export type NextHandleFunction = (
|
||||
req: http.IncomingMessage & IncomingMessageExtended,
|
||||
res: http.ServerResponse,
|
||||
next: NextFunction,
|
||||
) => void;
|
||||
|
||||
export type Handler = SimpleHandleFunction | NextHandleFunction;
|
||||
|
||||
export const fastifyMiddie: FastifyMiddie;
|
||||
export { fastifyMiddie as default };
|
||||
}
|
||||
|
||||
declare module 'fastify' {
|
||||
interface FastifyInstance {
|
||||
use(fn: fastifyMiddie.Handler): this;
|
||||
use(route: string, fn: fastifyMiddie.Handler): this;
|
||||
use(routes: string[], fn: fastifyMiddie.Handler): this;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A clone of `@fastify/middie` engine https://github.com/fastify/middie
|
||||
* with an extra vulnerability fix. Path is now decoded before matching to
|
||||
* avoid bypassing middleware with encoded characters.
|
||||
*/
|
||||
export default (fp as any)(fastifyMiddie, {
|
||||
fastify: '5.x',
|
||||
name: '@fastify/middie',
|
||||
}) as unknown as typeof fastifyMiddie;
|
||||
|
||||
export { fastifyMiddie };
|
||||
@@ -27,13 +27,12 @@
|
||||
"dependencies": {
|
||||
"@fastify/cors": "11.3.0",
|
||||
"@fastify/formbody": "9.0.0",
|
||||
"@fastify/middie": "9.3.4",
|
||||
"fast-querystring": "1.1.2",
|
||||
"fastify": "5.12.4",
|
||||
"fastify-plugin": "6.0.0",
|
||||
"find-my-way": "9.9.0",
|
||||
"light-my-request": "6.6.0",
|
||||
"path-to-regexp": "8.4.2",
|
||||
"reusify": "1.1.0",
|
||||
"tslib": "2.8.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { fastifyMiddie } from '../../adapters/middie/fastify-middie';
|
||||
import { fastifyMiddie } from '@fastify/middie';
|
||||
|
||||
describe('@fastify/middie', () => {
|
||||
it('keeps inherited middleware paths stable across nested prefixes', () => {
|
||||
|
||||
Reference in new issue
Block a user