@@ -3,7 +3,7 @@ import fs from 'fs'
3
3
import { IncomingMessage , ServerResponse } from 'http'
4
4
import Proxy from 'http-proxy'
5
5
import nanoid from 'next/dist/compiled/nanoid/index.js'
6
- import { join , resolve , sep } from 'path'
6
+ import { join , relative , resolve , sep } from 'path'
7
7
import { parse as parseQs , ParsedUrlQuery } from 'querystring'
8
8
import { format as formatUrl , parse as parseUrl , UrlWithParsedQuery } from 'url'
9
9
import { PrerenderManifest } from '../../build'
@@ -346,7 +346,11 @@ export default class Server {
346
346
match : route ( '/static/:path*' ) ,
347
347
name : 'static catchall' ,
348
348
fn : async ( req , res , params , parsedUrl ) => {
349
- const p = join ( this . dir , 'static' , ...( params . path || [ ] ) )
349
+ const p = join (
350
+ this . dir ,
351
+ 'static' ,
352
+ ...( params . path || [ ] ) . map ( encodeURIComponent )
353
+ )
350
354
await this . serveStatic ( req , res , p , parsedUrl )
351
355
return {
352
356
finished : true ,
@@ -705,14 +709,15 @@ export default class Server {
705
709
match : route ( '/:path*' ) ,
706
710
name : 'public folder catchall' ,
707
711
fn : async ( req , res , params , parsedUrl ) => {
708
- const path = `/${ ( params . path || [ ] ) . join ( '/' ) } `
712
+ const pathParts : string [ ] = params . path || [ ]
713
+ const path = `/${ pathParts . join ( '/' ) } `
709
714
710
715
if ( publicFiles . has ( path ) ) {
711
716
await this . serveStatic (
712
717
req ,
713
718
res ,
714
719
// we need to re-encode it since send decodes it
715
- join ( this . dir , 'public' , encodeURIComponent ( path ) ) ,
720
+ join ( this . publicDir , ... pathParts . map ( encodeURIComponent ) ) ,
716
721
parsedUrl
717
722
)
718
723
return {
@@ -1350,18 +1355,77 @@ export default class Server {
1350
1355
}
1351
1356
}
1352
1357
1353
- private isServeableUrl ( path : string ) : boolean {
1354
- const resolved = resolve ( path )
1358
+ private _validFilesystemPathSet : Set < string > | null = null
1359
+ private getFilesystemPaths ( ) : Set < string > {
1360
+ if ( this . _validFilesystemPathSet ) {
1361
+ return this . _validFilesystemPathSet
1362
+ }
1363
+
1364
+ const pathUserFilesStatic = join ( this . dir , 'static' )
1365
+ let userFilesStatic : string [ ] = [ ]
1366
+ if ( this . hasStaticDir && fs . existsSync ( pathUserFilesStatic ) ) {
1367
+ userFilesStatic = recursiveReadDirSync ( pathUserFilesStatic ) . map ( f =>
1368
+ join ( '.' , 'static' , f )
1369
+ )
1370
+ }
1371
+
1372
+ let userFilesPublic : string [ ] = [ ]
1373
+ if ( this . publicDir && fs . existsSync ( this . publicDir ) ) {
1374
+ userFilesPublic = recursiveReadDirSync ( this . publicDir ) . map ( f =>
1375
+ join ( '.' , 'public' , f )
1376
+ )
1377
+ }
1378
+
1379
+ let nextFilesStatic : string [ ] = [ ]
1380
+ nextFilesStatic = recursiveReadDirSync (
1381
+ join ( this . distDir , 'static' )
1382
+ ) . map ( f => join ( '.' , relative ( this . dir , this . distDir ) , 'static' , f ) )
1383
+
1384
+ return ( this . _validFilesystemPathSet = new Set < string > ( [
1385
+ ...nextFilesStatic ,
1386
+ ...userFilesPublic ,
1387
+ ...userFilesStatic ,
1388
+ ] ) )
1389
+ }
1390
+
1391
+ protected isServeableUrl ( untrustedFileUrl : string ) : boolean {
1392
+ // This method mimics what the version of `send` we use does:
1393
+ // 1. decodeURIComponent:
1394
+ // https://github.com/pillarjs/send/blob/0.17.1/index.js#L989
1395
+ // https://github.com/pillarjs/send/blob/0.17.1/index.js#L518-L522
1396
+ // 2. resolve:
1397
+ // https://github.com/pillarjs/send/blob/de073ed3237ade9ff71c61673a34474b30e5d45b/index.js#L561
1398
+
1399
+ let decodedUntrustedFilePath : string
1400
+ try {
1401
+ // (1) Decode the URL so we have the proper file name
1402
+ decodedUntrustedFilePath = decodeURIComponent ( untrustedFileUrl )
1403
+ } catch {
1404
+ return false
1405
+ }
1406
+
1407
+ // (2) Resolve "up paths" to determine real request
1408
+ const untrustedFilePath = resolve ( decodedUntrustedFilePath )
1409
+
1410
+ // don't allow null bytes anywhere in the file path
1411
+ if ( untrustedFilePath . indexOf ( '\0' ) !== - 1 ) {
1412
+ return false
1413
+ }
1414
+
1415
+ // Check if .next/static, static and public are in the path.
1416
+ // If not the path is not available.
1355
1417
if (
1356
- resolved . indexOf ( join ( this . distDir ) + sep ) !== 0 &&
1357
- resolved . indexOf ( join ( this . dir , 'static' ) + sep ) !== 0 &&
1358
- resolved . indexOf ( join ( this . dir , 'public' ) + sep ) !== 0
1418
+ ( untrustedFilePath . startsWith ( join ( this . distDir , 'static' ) + sep ) ||
1419
+ untrustedFilePath . startsWith ( join ( this . dir , 'static' ) + sep ) ||
1420
+ untrustedFilePath . startsWith ( join ( this . dir , 'public' ) + sep ) ) === false
1359
1421
) {
1360
- // Seems like the user is trying to traverse the filesystem.
1361
1422
return false
1362
1423
}
1363
1424
1364
- return true
1425
+ // Check against the real filesystem paths
1426
+ const filesystemUrls = this . getFilesystemPaths ( )
1427
+ const resolved = relative ( this . dir , untrustedFilePath )
1428
+ return filesystemUrls . has ( resolved )
1365
1429
}
1366
1430
1367
1431
protected readBuildId ( ) : string {
0 commit comments