@@ -3,14 +3,47 @@ import fs from 'node:fs'
33import fsp from 'node:fs/promises'
44import convertSourceMap from 'convert-source-map'
55import type { ExistingRawSourceMap , SourceMap } from 'rolldown'
6+ import colors from 'picocolors'
67import type { Logger } from '../logger'
7- import { blankReplacer , createDebugger } from '../utils'
8+ import {
9+ blankReplacer ,
10+ createDebugger ,
11+ isParentDirectory ,
12+ normalizePath ,
13+ } from '../utils'
814import { cleanUrl } from '../../shared/utils'
915
1016const debug = createDebugger ( 'vite:sourcemap' , {
1117 onlyWhenFocused : true ,
1218} )
1319
20+ /**
21+ * Given a file path inside node_modules, returns the package root directory.
22+ * For scoped packages like `node_modules/@scope/pkg/dist/foo.js`, returns `node_modules/@scope/pkg`.
23+ * Returns `undefined` if the file is not inside node_modules.
24+ */
25+ export function getNodeModulesPackageRoot (
26+ filePath : string ,
27+ ) : string | undefined {
28+ const normalized = normalizePath ( filePath )
29+ const nodeModulesIndex = normalized . lastIndexOf ( '/node_modules/' )
30+ if ( nodeModulesIndex === - 1 ) return undefined
31+
32+ const packageStart = nodeModulesIndex + '/node_modules/' . length
33+ const rest = normalized . slice ( packageStart )
34+ const firstSlash = rest . indexOf ( '/' )
35+
36+ let packageName : string
37+ if ( rest . startsWith ( '@' ) ) {
38+ // scoped package: @scope /pkg
39+ const secondSlash = rest . indexOf ( '/' , firstSlash + 1 )
40+ packageName = secondSlash === - 1 ? rest : rest . slice ( 0 , secondSlash )
41+ } else {
42+ packageName = firstSlash === - 1 ? rest : rest . slice ( 0 , firstSlash )
43+ }
44+ return normalized . slice ( 0 , packageStart ) + packageName
45+ }
46+
1447// Virtual modules should be prefixed with a null byte to avoid a
1548// false positive "missing source" warning. We also check for certain
1649// prefixes used for special handling in esbuildDepPlugin.
@@ -40,6 +73,7 @@ export async function injectSourcesContent(
4073) : Promise < void > {
4174 let sourceRootPromise : Promise < string | undefined >
4275
76+ const packageRoot = getNodeModulesPackageRoot ( file )
4377 const missingSources : string [ ] = [ ]
4478 const sourcesContent = map . sourcesContent || [ ]
4579 const sourcesContentPromises : Promise < void > [ ] = [ ]
@@ -59,7 +93,22 @@ export async function injectSourcesContent(
5993 if ( sourceRoot ) {
6094 resolvedSourcePath = path . resolve ( sourceRoot , resolvedSourcePath )
6195 }
62-
96+ // Block path traversal outside the package boundary for node_modules
97+ // A malicious package may point to a sensitive file
98+ if ( packageRoot ) {
99+ const resolvedSourcePathNormalized = normalizePath (
100+ path . resolve ( resolvedSourcePath ) ,
101+ )
102+ if ( ! isParentDirectory ( packageRoot , resolvedSourcePathNormalized ) ) {
103+ sourcesContent [ index ] = null
104+ logger . warnOnce (
105+ colors . yellow (
106+ `Sourcemap for ${ JSON . stringify ( file ) } points to a source file outside its package: ${ JSON . stringify ( resolvedSourcePathNormalized ) } ` ,
107+ ) ,
108+ )
109+ return
110+ }
111+ }
63112 sourcesContent [ index ] = await fsp
64113 . readFile ( resolvedSourcePath , 'utf-8' )
65114 . catch ( ( ) => {
@@ -153,12 +202,13 @@ export function applySourcemapIgnoreList(
153202export function extractSourcemapFromFile (
154203 code : string ,
155204 filePath : string ,
205+ logger : Logger ,
156206) : { code : string ; map : SourceMap } | undefined {
157207 const map = (
158208 convertSourceMap . fromSource ( code ) ||
159209 convertSourceMap . fromMapFileSource (
160210 code ,
161- createConvertSourceMapReadMap ( filePath ) ,
211+ createConvertSourceMapReadMap ( filePath , logger ) ,
162212 )
163213 ) ?. toObject ( )
164214
@@ -170,11 +220,24 @@ export function extractSourcemapFromFile(
170220 }
171221}
172222
173- function createConvertSourceMapReadMap ( originalFileName : string ) {
223+ function createConvertSourceMapReadMap (
224+ originalFileName : string ,
225+ logger : Logger ,
226+ ) {
227+ const packageRoot = getNodeModulesPackageRoot ( originalFileName )
174228 return ( filename : string ) => {
175- return fs . readFileSync (
176- path . resolve ( path . dirname ( originalFileName ) , filename ) ,
177- 'utf-8' ,
178- )
229+ const resolvedPath = path . resolve ( path . dirname ( originalFileName ) , filename )
230+ if (
231+ packageRoot &&
232+ ! isParentDirectory ( packageRoot , normalizePath ( resolvedPath ) )
233+ ) {
234+ logger . warnOnce (
235+ colors . yellow (
236+ `Sourcemap in "${ originalFileName } " references a map file outside its package: "${ filename } "` ,
237+ ) ,
238+ )
239+ return '{}'
240+ }
241+ return fs . readFileSync ( resolvedPath , 'utf-8' )
179242 }
180243}
0 commit comments