11import { createPackageFromStreams , AsarStreamType , AsarDirectory } from "@electron/asar"
22import { log } from "builder-util"
3- import { Filter } from "builder-util/out/fs"
3+ import { exists , Filter } from "builder-util/out/fs"
44import * as fs from "fs-extra"
55import { readlink } from "fs-extra"
66import * as path from "path"
@@ -11,6 +11,43 @@ import { detectUnpackedDirs } from "./unpackDetector"
1111import { Readable } from "stream"
1212import * as os from "os"
1313
14+ const DENYLIST = Promise . all (
15+ [
16+ "/usr" ,
17+ "/lib" ,
18+ "/bin" ,
19+ "/sbin" ,
20+ "/etc" ,
21+
22+ "/tmp" ,
23+ "/var" , // block whole /var by default. If $HOME is under /var, it's explicitly in ALLOWLIST
24+
25+ // macOS system directories
26+ "/System" ,
27+ "/Library" ,
28+ "/private" ,
29+
30+ // Windows system directories
31+ process . env . SystemRoot ,
32+ process . env . WINDIR ,
33+ ]
34+ . filter ( ( it ) : it is string => it != null )
35+ . map ( async it => ( ( await exists ( it ) ) ? await resolvePath ( it ) : null ) )
36+ . filter ( ( it ) : it is Promise < string > => it != null )
37+ )
38+
39+ const ALLOWLIST = Promise . all (
40+ [
41+ process . env . HOME , // always allow current user’s home
42+ os . tmpdir ( ) , // always allow temp directory
43+ ]
44+ . filter ( ( it ) : it is string => it != null )
45+ . map ( async it => ( ( await exists ( it ) ) ? await resolvePath ( it ) : null ) )
46+ . filter ( ( it ) : it is Promise < string > => it != null )
47+ )
48+
49+ const resolvePath = ( file : string ) => fs . realpath ( file ) . catch ( ( ) => path . resolve ( file ) )
50+
1451/** @internal */
1552export class AsarPackager {
1653 private readonly outFile : string
@@ -148,7 +185,7 @@ export class AsarPackager {
148185 }
149186
150187 // verify that the file is not a direct link or symlinked to access/copy a system file
151- await this . protectSystemAndUnsafePaths ( file )
188+ await this . protectSystemAndUnsafePaths ( file , await this . packager . info . getWorkspaceRoot ( ) )
152189
153190 const config = {
154191 path : destination ,
@@ -165,9 +202,6 @@ export class AsarPackager {
165202 }
166203 }
167204
168- // guard against symlink pointing to outside workspace root
169- await this . protectSystemAndUnsafePaths ( file , await this . packager . info . getWorkspaceRoot ( ) )
170-
171205 // okay, it must be a symlink. evaluate link to be relative to source file in asar
172206 let link = await readlink ( file )
173207 if ( path . isAbsolute ( link ) ) {
@@ -233,80 +267,41 @@ export class AsarPackager {
233267 }
234268 }
235269
236- private async getProtectedPaths ( ) : Promise < string [ ] > {
237- const systemPaths = [
238- // Generic *nix
239- "/usr" ,
240- "/lib" ,
241- "/bin" ,
242- "/sbin" ,
243- "/System" ,
244- "/Library" ,
245- "/private/etc" ,
246- "/private/var/db" ,
247- "/private/var/root" ,
248- "/private/var/log" ,
249- "/private/tmp" ,
250-
251- // macOS legacy symlinks
252- "/etc" ,
253- "/var" ,
254- "/tmp" ,
255-
256- // Windows
257- process . env . SystemRoot ,
258- process . env . WINDIR ,
259- // process.env.ProgramFiles,
260- // process.env["ProgramFiles(x86)"],
261- // process.env.ProgramData,
262- // process.env.CommonProgramFiles,
263- // process.env["CommonProgramFiles(x86)"],
264- ]
265- . filter ( Boolean )
266- . map ( p => path . resolve ( p as string ) )
267-
268- // Normalize to real paths to prevent symlink bypasses
269- const resolvedPaths : string [ ] = [ ]
270- for ( const p of systemPaths ) {
271- try {
272- resolvedPaths . push ( await fs . realpath ( p ) )
273- } catch {
274- resolvedPaths . push ( path . resolve ( p ) )
270+ private async checkAgainstRoots ( target : string , allowRoots : string [ ] ) : Promise < boolean > {
271+ const resolved = await resolvePath ( target )
272+
273+ for ( const root of allowRoots ) {
274+ const resolvedRoot = root
275+ if ( resolved === resolvedRoot || resolved . startsWith ( resolvedRoot + path . sep ) ) {
276+ return true
275277 }
276278 }
277-
278- return resolvedPaths
279+ return false
279280 }
280281
281- private async protectSystemAndUnsafePaths ( file : string , workspaceRoot ? : string ) : Promise < boolean > {
282- const resolved = await fs . realpath ( file ) . catch ( ( ) => path . resolve ( file ) )
282+ private async protectSystemAndUnsafePaths ( file : string , workspaceRoot : string ) : Promise < boolean > {
283+ const resolved = await resolvePath ( file )
283284
284- const scan = async ( ) => {
285- if ( workspaceRoot ) {
286- const workspace = path . resolve ( workspaceRoot )
285+ const isUnsafe = async ( ) => {
286+ const workspace = await resolvePath ( workspaceRoot )
287287
288- if ( ! resolved . startsWith ( workspace ) ) {
289- return true
290- }
291- }
292-
293- // Allow temp & cache folders
294- const tmpdir = await fs . realpath ( os . tmpdir ( ) )
295- if ( resolved . startsWith ( tmpdir ) ) {
288+ if ( resolved . startsWith ( workspace ) ) {
296289 return false
297290 }
298291
299- const blockedSystemPaths = await this . getProtectedPaths ( )
300- for ( const sys of blockedSystemPaths ) {
301- if ( resolved . startsWith ( sys ) ) {
302- return true
303- }
304- }
292+ const denied = await this . checkAgainstRoots ( file , await DENYLIST )
293+ const allowed = await this . checkAgainstRoots ( file , await ALLOWLIST )
305294
295+ if ( allowed ) {
296+ return false // allowlist overrides everything
297+ }
298+ if ( denied ) {
299+ return true // blocked unless explicitly allowed
300+ }
306301 return false
307302 }
308303
309- const unsafe = await scan ( )
304+ const unsafe = await isUnsafe ( )
310305
311306 if ( unsafe ) {
312307 log . error ( { source : file , realPath : resolved } , `unable to copy, file is from outside the package to a system or unsafe path` )
0 commit comments