@@ -158,6 +158,7 @@ async function downloadIndex(cwd: string, version: string, options: DocsIndexPro
158158 }
159159 const archive = join ( staging , 'docs.tgz' )
160160 writeFileSync ( archive , tarball )
161+ assertSafeArchiveMembers ( archive )
161162 // `tar` ships with macOS, Linux and Windows 10 onwards. The archive's
162163 // `package/` wrapper is stripped so paths match an installed copy.
163164 execFileSync ( 'tar' , [ '-xzf' , archive , '-C' , staging , '--strip-components=1' ] , { stdio : 'ignore' } )
@@ -173,6 +174,24 @@ async function downloadIndex(cwd: string, version: string, options: DocsIndexPro
173174 }
174175}
175176
177+ /**
178+ * Refuse an archive whose member paths could escape the extraction directory:
179+ * absolute paths, drive letters, or `..` segments. The registry tarball is
180+ * still a download, and `tar`'s own handling of such members varies across
181+ * the system implementations this relies on.
182+ */
183+ function assertSafeArchiveMembers ( archive : string ) : void {
184+ const listing = execFileSync ( 'tar' , [ '-tzf' , archive ] , { encoding : 'utf8' , stdio : [ 'ignore' , 'pipe' , 'ignore' ] , maxBuffer : 16 * 1024 * 1024 } )
185+ for ( const member of listing . split ( '\n' ) ) {
186+ if ( ! member ) {
187+ continue
188+ }
189+ if ( member . startsWith ( '/' ) || member . startsWith ( '\\' ) || / ^ [ a - z ] : / i. test ( member ) || member . split ( / [ \\ / ] / ) . includes ( '..' ) ) {
190+ throw new Error ( `Refusing to extract unsafe archive member \`${ member } \`` )
191+ }
192+ }
193+ }
194+
176195/**
177196 * The `@nuxt/docs` tarball from the configured registry, falling back to npm
178197 * itself: the docs are a public package, so a proxy that rejects this process is
@@ -277,13 +296,17 @@ function writeCache(nuxtVersion: string | undefined, index: DocsIndex): void {
277296 }
278297}
279298
299+ /**
300+ * Only regular files and directories are considered: a symlink in the archive
301+ * would otherwise be read through, reaching outside the extracted tree.
302+ */
280303function * markdownFiles ( dir : string ) : Generator < string > {
281304 for ( const item of readdirSync ( dir , { withFileTypes : true } ) ) {
282305 const path = join ( dir , item . name )
283306 if ( item . isDirectory ( ) ) {
284307 yield * markdownFiles ( path )
285308 }
286- else if ( item . name . endsWith ( '.md' ) && item . name !== 'README.md' ) {
309+ else if ( item . isFile ( ) && item . name . endsWith ( '.md' ) && item . name !== 'README.md' ) {
287310 yield path
288311 }
289312 }
0 commit comments