Skip to content

Commit 0cd0831

Browse files
authored
fix: allow home dir when it's symlinked from /var/home (#9389)
1 parent 65eecac commit 0cd0831

File tree

2 files changed

+63
-67
lines changed

2 files changed

+63
-67
lines changed

.changeset/fast-parrots-turn.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"app-builder-lib": patch
3+
---
4+
5+
fix: allow home dir when it's symlinked from /var/home

packages/app-builder-lib/src/asar/asarUtil.ts

Lines changed: 58 additions & 67 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { createPackageFromStreams, AsarStreamType, AsarDirectory } from "@electron/asar"
22
import { log } from "builder-util"
3-
import { Filter } from "builder-util/out/fs"
3+
import { exists, Filter } from "builder-util/out/fs"
44
import * as fs from "fs-extra"
55
import { readlink } from "fs-extra"
66
import * as path from "path"
@@ -11,6 +11,36 @@ import { detectUnpackedDirs } from "./unpackDetector"
1111
import { Readable } from "stream"
1212
import * as os from "os"
1313

14+
const resolvePath = async (file: string | undefined): Promise<string | undefined> => (file && (await exists(file)) ? fs.realpath(file).catch(() => path.resolve(file)) : undefined)
15+
const resolvePaths = async (filepaths: (string | undefined)[]) => {
16+
return Promise.all(filepaths.map(resolvePath)).then(paths => paths.filter((it): it is string => it != null))
17+
}
18+
19+
const DENYLIST = resolvePaths([
20+
"/usr",
21+
"/lib",
22+
"/bin",
23+
"/sbin",
24+
"/etc",
25+
26+
"/tmp",
27+
"/var", // block whole /var by default. If $HOME is under /var, it's explicitly in ALLOWLIST - https://github.com/electron-userland/electron-builder/issues/9025#issuecomment-3575380041
28+
29+
// macOS system directories
30+
"/System",
31+
"/Library",
32+
"/private",
33+
34+
// Windows system directories
35+
process.env.SystemRoot,
36+
process.env.WINDIR,
37+
])
38+
39+
const ALLOWLIST = resolvePaths([
40+
os.tmpdir(), // always allow temp dir
41+
os.homedir(), // always allow home dir
42+
])
43+
1444
/** @internal */
1545
export class AsarPackager {
1646
private readonly outFile: string
@@ -148,7 +178,7 @@ export class AsarPackager {
148178
}
149179

150180
// verify that the file is not a direct link or symlinked to access/copy a system file
151-
await this.protectSystemAndUnsafePaths(file)
181+
await this.protectSystemAndUnsafePaths(file, await this.packager.info.getWorkspaceRoot())
152182

153183
const config = {
154184
path: destination,
@@ -165,9 +195,6 @@ export class AsarPackager {
165195
}
166196
}
167197

168-
// guard against symlink pointing to outside workspace root
169-
await this.protectSystemAndUnsafePaths(file, await this.packager.info.getWorkspaceRoot())
170-
171198
// okay, it must be a symlink. evaluate link to be relative to source file in asar
172199
let link = await readlink(file)
173200
if (path.isAbsolute(link)) {
@@ -233,85 +260,49 @@ export class AsarPackager {
233260
}
234261
}
235262

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))
263+
private async checkAgainstRoots(target: string, allowRoots: string[]): Promise<boolean> {
264+
const resolved = await resolvePath(target)
265+
266+
for (const root of allowRoots) {
267+
const resolvedRoot = root
268+
if (resolved === resolvedRoot || resolved?.startsWith(resolvedRoot + path.sep)) {
269+
return true
275270
}
276271
}
277-
278-
return resolvedPaths
272+
return false
279273
}
280274

281-
private async protectSystemAndUnsafePaths(file: string, workspaceRoot?: string): Promise<boolean> {
282-
const resolved = await fs.realpath(file).catch(() => path.resolve(file))
283-
284-
const scan = async () => {
285-
if (workspaceRoot) {
286-
const workspace = path.resolve(workspaceRoot)
275+
private async protectSystemAndUnsafePaths(file: string, workspaceRoot: string): Promise<void> {
276+
const resolved = await resolvePath(file)
277+
const logFields = { source: file, realPath: resolved }
287278

288-
if (!resolved.startsWith(workspace)) {
289-
return true
290-
}
291-
}
279+
const isUnsafe = async () => {
280+
const workspace = await resolvePath(workspaceRoot)
292281

293-
// Allow temp & cache folders
294-
const tmpdir = await fs.realpath(os.tmpdir())
295-
if (resolved.startsWith(tmpdir)) {
282+
if (workspace && resolved?.startsWith(workspace)) {
283+
// if in workspace, always safe
296284
return false
297285
}
298286

299-
const blockedSystemPaths = await this.getProtectedPaths()
300-
for (const sys of blockedSystemPaths) {
301-
if (resolved.startsWith(sys)) {
302-
return true
303-
}
287+
const allowed = await this.checkAgainstRoots(file, await ALLOWLIST)
288+
if (allowed) {
289+
return false // allowlist is priority
304290
}
305291

292+
const denied = await this.checkAgainstRoots(file, await DENYLIST)
293+
if (denied) {
294+
log.error(logFields, `denied access to system or unsafe path`)
295+
return true
296+
}
297+
// default
298+
log.debug(logFields, `path is outside of explicit safe paths, defaulting to safe`)
306299
return false
307300
}
308301

309-
const unsafe = await scan()
302+
const unsafe = await isUnsafe()
310303

311304
if (unsafe) {
312-
log.error({ source: file, realPath: resolved }, `unable to copy, file is from outside the package to a system or unsafe path`)
313305
throw new Error(`Cannot copy file [${file}] symlinked to file [${resolved}] outside the package to a system or unsafe path`)
314306
}
315-
return unsafe
316307
}
317308
}

0 commit comments

Comments
 (0)