@@ -3,7 +3,7 @@ import fs from 'fs'
33import { IncomingMessage , ServerResponse } from 'http'
44import Proxy from 'http-proxy'
55import nanoid from 'next/dist/compiled/nanoid/index.js'
6- import { join , resolve , sep } from 'path'
6+ import { join , relative , resolve , sep } from 'path'
77import { parse as parseQs , ParsedUrlQuery } from 'querystring'
88import { format as formatUrl , parse as parseUrl , UrlWithParsedQuery } from 'url'
99import { PrerenderManifest } from '../../build'
@@ -346,7 +346,11 @@ export default class Server {
346346 match : route ( '/static/:path*' ) ,
347347 name : 'static catchall' ,
348348 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+ )
350354 await this . serveStatic ( req , res , p , parsedUrl )
351355 return {
352356 finished : true ,
@@ -705,14 +709,15 @@ export default class Server {
705709 match : route ( '/:path*' ) ,
706710 name : 'public folder catchall' ,
707711 fn : async ( req , res , params , parsedUrl ) => {
708- const path = `/${ ( params . path || [ ] ) . join ( '/' ) } `
712+ const pathParts : string [ ] = params . path || [ ]
713+ const path = `/${ pathParts . join ( '/' ) } `
709714
710715 if ( publicFiles . has ( path ) ) {
711716 await this . serveStatic (
712717 req ,
713718 res ,
714719 // we need to re-encode it since send decodes it
715- join ( this . dir , 'public' , encodeURIComponent ( path ) ) ,
720+ join ( this . publicDir , ... pathParts . map ( encodeURIComponent ) ) ,
716721 parsedUrl
717722 )
718723 return {
@@ -1350,18 +1355,77 @@ export default class Server {
13501355 }
13511356 }
13521357
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.
13551417 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
13591421 ) {
1360- // Seems like the user is trying to traverse the filesystem.
13611422 return false
13621423 }
13631424
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 )
13651429 }
13661430
13671431 protected readBuildId ( ) : string {
0 commit comments