Skip to content

Commit 27f2bfe

Browse files
authored
fix: if serve: false and root is not defined, only allow sending files with absolute path (#540)
* fix: if serve: false and root is not defined, only allow sending files with absolute path * fix * fix If serve is set explicitly to false then root is required If root is set, it must be validated
1 parent ac71e33 commit 27f2bfe

File tree

5 files changed

+125
-36
lines changed

5 files changed

+125
-36
lines changed

README.md

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -138,7 +138,12 @@ fastify.get('/favicon.ico', function (req, reply) {
138138
139139
### Options
140140
141-
#### `root` (required)
141+
#### `serve`
142+
Default: `true`
143+
144+
If set to `false`, the plugin will not serve files from the `root` directory.
145+
146+
#### `root` (required if `serve` is not false)
142147
143148
The absolute path of the directory containing the files to serve.
144149
The file to serve is determined by combining `req.url` with the

index.js

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,8 +23,10 @@ const encodingExtensionMap = {
2323

2424
/** @type {import("fastify").FastifyPluginAsync<import("./types").FastifyStaticOptions>} */
2525
async function fastifyStatic (fastify, opts) {
26-
opts.root = normalizeRoot(opts.root)
27-
checkRootPathForErrors(fastify, opts.root)
26+
if (opts.serve !== false || opts.root !== undefined) {
27+
opts.root = normalizeRoot(opts.root)
28+
checkRootPathForErrors(fastify, opts.root)
29+
}
2830

2931
const setHeaders = opts.setHeaders
3032
if (setHeaders !== undefined && typeof setHeaders !== 'function') {
@@ -201,6 +203,8 @@ async function fastifyStatic (fastify, opts) {
201203
} else {
202204
options.root = rootPath
203205
}
206+
} else if (path.isAbsolute(pathname) === false) {
207+
return reply.callNotFound()
204208
}
205209

206210
if (allowedPath && !allowedPath(pathname, options.root, request)) {

test/static.test.js

Lines changed: 56 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -753,6 +753,51 @@ test('serving disabled', async (t) => {
753753
})
754754
})
755755

756+
test('serving disabled without root', async (t) => {
757+
t.plan(2)
758+
759+
const pluginOptions = {
760+
prefix: '/static/',
761+
serve: false
762+
}
763+
const fastify = Fastify()
764+
fastify.register(fastifyStatic, pluginOptions)
765+
766+
t.after(() => fastify.close())
767+
768+
fastify.get('/foo/bar/r', (_request, reply) => {
769+
reply.sendFile('index.html')
770+
})
771+
772+
fastify.get('/foo/bar/a', (_request, reply) => {
773+
reply.sendFile(path.join(__dirname, pluginOptions.prefix, 'index.html'))
774+
})
775+
776+
t.after(() => fastify.close())
777+
778+
await fastify.listen({ port: 0 })
779+
780+
fastify.server.unref()
781+
782+
await t.test('/static/index.html via sendFile not found', async (t) => {
783+
t.plan(3 + GENERIC_RESPONSE_CHECK_COUNT)
784+
785+
const response = await fetch('http://localhost:' + fastify.server.address().port + '/foo/bar/a')
786+
t.assert.ok(response.ok)
787+
t.assert.deepStrictEqual(response.status, 200)
788+
t.assert.deepStrictEqual(await response.text(), indexContent)
789+
genericResponseChecks(t, response)
790+
})
791+
792+
await t.test('/static/index.html via sendFile with relative path not found', async (t) => {
793+
t.plan(2)
794+
795+
const response = await fetch('http://localhost:' + fastify.server.address().port + '/foo/bar/r')
796+
t.assert.ok(!response.ok)
797+
t.assert.deepStrictEqual(response.status, 404)
798+
})
799+
})
800+
756801
test('sendFile', async (t) => {
757802
t.plan(4)
758803

@@ -1215,7 +1260,7 @@ test('maxAge option', async (t) => {
12151260
})
12161261

12171262
test('errors', async (t) => {
1218-
t.plan(11)
1263+
t.plan(12)
12191264

12201265
await t.test('no root', async (t) => {
12211266
t.plan(1)
@@ -1280,6 +1325,16 @@ test('errors', async (t) => {
12801325
await t.assert.rejects(async () => await fastify.register(fastifyStatic, pluginOptions))
12811326
})
12821327

1328+
await t.test('no root and serve: false', async (t) => {
1329+
t.plan(1)
1330+
const pluginOptions = {
1331+
serve: false,
1332+
root: []
1333+
}
1334+
const fastify = Fastify({ logger: false })
1335+
await t.assert.rejects(async () => await fastify.register(fastifyStatic, pluginOptions))
1336+
})
1337+
12831338
await t.test('duplicate root paths are not allowed', async (t) => {
12841339
t.plan(1)
12851340
const pluginOptions = {

types/index.d.ts

Lines changed: 44 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -84,40 +84,52 @@ declare namespace fastifyStatic {
8484
serveDotFiles?: boolean;
8585
}
8686

87-
export interface FastifyStaticOptions extends SendOptions {
88-
root: string | string[] | URL | URL[];
89-
prefix?: string;
90-
prefixAvoidTrailingSlash?: boolean;
91-
serve?: boolean;
92-
decorateReply?: boolean;
93-
schemaHide?: boolean;
94-
setHeaders?: (res: SetHeadersResponse, path: string, stat: Stats) => void;
95-
redirect?: boolean;
96-
wildcard?: boolean;
97-
globIgnore?: string[];
98-
list?: boolean | ListOptionsJsonFormat | ListOptionsHtmlFormat;
99-
allowedPath?: (pathName: string, root: string, request: FastifyRequest) => boolean;
100-
/**
101-
* @description
102-
* Opt-in to looking for pre-compressed files
103-
*/
104-
preCompressed?: boolean;
105-
106-
// Passed on to `send`
107-
acceptRanges?: boolean;
108-
contentType?: boolean;
109-
cacheControl?: boolean;
110-
dotfiles?: 'allow' | 'deny' | 'ignore';
111-
etag?: boolean;
112-
extensions?: string[];
113-
immutable?: boolean;
114-
index?: string[] | string | false;
115-
lastModified?: boolean;
116-
maxAge?: string | number;
117-
constraints?: RouteOptions['constraints'];
118-
logLevel?: RouteOptions['logLevel'];
87+
type Root = string | string[] | URL | URL[]
88+
89+
type RootOptions = {
90+
serve: true;
91+
root: Root;
92+
} | {
93+
serve?: false;
94+
root?: Root;
11995
}
12096

97+
export type FastifyStaticOptions =
98+
SendOptions
99+
& RootOptions
100+
& {
101+
// Added by this plugin
102+
prefix?: string;
103+
prefixAvoidTrailingSlash?: boolean;
104+
decorateReply?: boolean;
105+
schemaHide?: boolean;
106+
setHeaders?: (res: SetHeadersResponse, path: string, stat: Stats) => void;
107+
redirect?: boolean;
108+
wildcard?: boolean;
109+
globIgnore?: string[];
110+
list?: boolean | ListOptionsJsonFormat | ListOptionsHtmlFormat;
111+
allowedPath?: (pathName: string, root: string, request: FastifyRequest) => boolean;
112+
/**
113+
* @description
114+
* Opt-in to looking for pre-compressed files
115+
*/
116+
preCompressed?: boolean;
117+
118+
// Passed on to `send`
119+
acceptRanges?: boolean;
120+
contentType?: boolean;
121+
cacheControl?: boolean;
122+
dotfiles?: 'allow' | 'deny' | 'ignore';
123+
etag?: boolean;
124+
extensions?: string[];
125+
immutable?: boolean;
126+
index?: string[] | string | false;
127+
lastModified?: boolean;
128+
maxAge?: string | number;
129+
constraints?: RouteOptions['constraints'];
130+
logLevel?: RouteOptions['logLevel'];
131+
}
132+
121133
export const fastifyStatic: FastifyStaticPlugin
122134

123135
export { fastifyStatic as default }

types/index.test-d.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,19 @@ expectAssignable<FastifyStaticOptions>({
120120
root: [new URL('')]
121121
})
122122

123+
expectError<FastifyStaticOptions>({
124+
serve: true
125+
})
126+
127+
expectAssignable<FastifyStaticOptions>({
128+
serve: true,
129+
root: ''
130+
})
131+
132+
expectAssignable<FastifyStaticOptions>({
133+
serve: false
134+
})
135+
123136
appWithImplicitHttp
124137
.register(fastifyStatic, options)
125138
.after(() => {

0 commit comments

Comments
 (0)