Skip to content

Extract Electron ASAR archives - #201

Open
davidnewhall wants to merge 4 commits into
mainfrom
feat/asar-extract
Open

davidnewhall wants to merge 4 commits into
mainfrom
feat/asar-extract

Conversation

@davidnewhall

Copy link
Copy Markdown
Contributor

Summary

  • Recognize .asar and extract through ExtractASAR: directories and package-relative symlinks first, then packed/unpacked file copies.
  • Parallel copies use the existing dispatchWorkers pool when FileWorkers > 1. Unpacked members come from {archive}.asar.unpacked; a missing sibling is an error that names the entry.
  • Depends on golift/asar#1 and the vanity mapping in golift/turbovanityurls#33.

Test plan

  • go test -race -run ASAR ./...
  • Sequential (FileWorkers 0/1) and parallel (FileWorkers 4) both write shared-offset files intact
  • Symlink targets that leave the output directory return ErrInvalidPath
  • Missing {archive}.asar.unpacked member fails without writing an empty stand-in

Made with Cursor

davidnewhall and others added 2 commits September 21, 2026 21:09
Use golift.io/asar for the pickle/JSON index, then mkdir and create in-tree symlinks sequentially before copying packed and unpacked files. FileWorkers greater than 1 shares dispatchWorkers with ZIP and 7z.

Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot review overview

🟡 Changes recommended

Unpacked inputs can escape through symlinks, and DeleteOrig leaves the consumed sidecar directory behind.

Get a fresh assessment by requesting another Copilot review.

Review effort: Balanced
Findings: 1 High severity · 1 Medium severity

Open (2)
What changed in this PR

Adds extraction support for Electron ASAR archives, including packed files, unpacked siblings, symlinks, and parallel workers.

Changes:

  • Registers .asar archives and adds the ASAR dependency.
  • Implements secure output paths, progress tracking, and parallel extraction.
  • Adds ASAR extraction tests.
File Description
asar.go Implements ASAR extraction.
asar_test.go Tests packed, unpacked, symlink, and parallel cases.
files.go Registers .asar support.
start.go Documents ASAR worker support.
go.mod, go.sum Adds the ASAR dependency.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread asar.go Outdated
Comment on lines +180 to +184
if !pathWithin(root, srcPath) {
return nil, fmt.Errorf("%s: %w: %s", name, ErrInvalidPath, srcPath)
}

src, err := os.Open(srcPath)
Comment thread files.go Outdated
// The ones with double extensions that match a single (below) need to come first.
{Type: "7zip", Ext: ".7z", Fn: Extract7z},
{Type: "7zip", Ext: ".7z.001", Fn: Extract7z},
{Type: "asar", Ext: ".asar", Fn: ChngInt(ExtractASAR)},
Comment thread asar.go Outdated
return nil, fmt.Errorf("%s: %w: %s", name, ErrInvalidPath, srcPath)
}

src, err := os.Open(srcPath)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Small FYI, not a blocker: this os.Open follows a symlink planted inside the {archive}.asar.unpacked sibling. I reproduced it — pointing an unpacked entry at a file outside the unpacked dir copies that file's bytes into the output. Not exploitable in practice: whoever ships the archive owns that directory and can already read whatever they link, and extraction never writes through the link, so there's no traversal or overwrite. It's just the one place in an otherwise symlink-paranoid codebase that reads through a link. A one-line comment noting the assumption (or routing this through the no-follow open helper like the write path does) would keep the posture consistent.

@qwen-pr-bot qwen-pr-bot Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Approving — first look, head b763900 vs base 8cc8e92a.

What this PR actually contains (three distinct changes, not just "ASAR support"):

  • .asar registered in the extension table and routed to a new ExtractASAR: dirs and package-relative symlinks first, then packed and unpacked file copies, parallel via the existing dispatchWorkers pool when FileWorkers > 1.
  • A bump of golift/asar so unpacked entries that also carry an offset are read from the {archive}.asar.unpacked sibling instead of being treated as packed archive bytes (the parser now checks unpacked before offset).
  • Doc-comment fixes in files.go/start.go listing ASAR as a random-access format alongside ZIP and 7z.

I traced the data flow end to end rather than relying on the diff alone. asarProgress (header-declared totals, archive-file compressed size, entry count) feeds checkClaimedLimits, so a malicious header still fails the MaxBytes/MaxFiles/MaxRatio fast-fail; the runtime write path re-checks per byte. asarPrepareEntries does containment + mkdir + symlink before any writes, and both the entry-name and symlink-target containment checks engage after the asar library has already rejected any entry name containing /, \, . or .., so traversal isn't reachable through names at all. createASARSymlink computes a relative target from the symlink's own directory and createSymlink re-validates it lexically and by resolving planted links. For concurrency, File.Open() hands each worker its own SectionReader over the shared ReaderAt, so shared-offset entries (a.txt/b.txt both at offset 8) copy intact — that's the exact case the parallel test exercises. ExtractASAR follows the same shape as ExtractZIP, so the tracker/done()/error handling is house-standard, not a copy-paste hazard.

Executed validation at head b763900:

  • go test -race -run ASAR ./... → ok
  • go test -race -covermode=atomic ./... → ok, 142s, 76.2% coverage (mirrors the CI gotest job)
  • go vet ./... → clean; golangci-lint run ./... (v2.13.0, repo config) → 0 issues
  • go build ./..., go mod tidy → clean, no changes wanted
  • Focused experiments for paths the tests skip: a nested symlink target (dir/nested.txt) and an absolute one (/bin) both neutralize to relative links inside the output dir; the one genuine gap is the read through a link in the .unpacked sibling (inline note, non-blocking).
  • CI on this exact commit is green: golangci-lint (linux/windows/freebsd/darwin), go test -race (ubuntu/macos/windows), Snyk.

One non-blocking note inline; nothing blocks approval.

…ith the archive.

os.Open followed links and special files out of {archive}.unpacked, and DeleteOrig only removed the archive path.

Co-authored-by: Cursor <cursoragent@cursor.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants