Skip to content

Commit 841e36e

Browse files
committed
Create ALLOWLIST to prioritize over DENYLIST to account for when HOME dir is within a system dir via symlink (#9025) [ such as with Fedora Silverblue ]
1 parent 7f7113d commit 841e36e

File tree

2 files changed

+66
-66
lines changed

2 files changed

+66
-66
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: 61 additions & 66 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,43 @@ import { detectUnpackedDirs } from "./unpackDetector"
1111
import { Readable } from "stream"
1212
import * 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 */
1552
export 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

Comments
 (0)