Skip to content

Add opt-in owner permission normalization to @tryghost/zip extract#761

Open
sagzy wants to merge 3 commits into
mainfrom
add-perms-check
Open

Add opt-in owner permission normalization to @tryghost/zip extract#761
sagzy wants to merge 3 commits into
mainfrom
add-perms-check

Conversation

@sagzy

@sagzy sagzy commented Jun 25, 2026

Copy link
Copy Markdown
Contributor

Summary

Adds an opt-in ensureOwnerPermissions option to @tryghost/zip's extract() that normalizes extracted entry permissions before extract-zip writes them. This fixes archives that contain read-only directories (e.g. dr-xr-xr-x / 0555) without changing default behavior for existing callers.

Why

Some user-supplied archives (e.g. theme zips) store directories as read-only (0555). When extract-zip recreates such a directory first and then tries to write a nested file into it, extraction fails with EACCES. Even when a read-only directory has no children, it lands on disk without owner write, so the caller can't subsequently move or remove the extracted tree.

A recursive post-extract chmod is not a robust primary fix, because the bad directory modes break extraction before any post-processing could run. Normalizing per-entry, just before the write, fixes the failure at the source.

What changed

packages/zip/lib/extract.js:

  • New options.ensureOwnerPermissions?: boolean (default false).
  • When true, each entry's mode is normalized in the wrapped onEntry callback:
    • Directories gain at least owner rwx (0o700).
    • Files gain at least owner rw (0o600).
    • Existing execute / group / world bits are preserved (owner-only bits are OR-ed in; nothing is widened for group/world).
    • Directory detection mirrors extract-zip exactly (POSIX dir bit, trailing slash, Windows directory attribute); inferred directories without POSIX type bits get the directory type bit set.
    • Entries with no POSIX mode bits are left untouched so extract-zip's own default modes still apply.
  • Only the in-memory entry handed to extract-zip is mutated — the original zip is never rewritten or chmodded.
  • Normalization runs after the existing size-limit, symlink, and filename checks and after the caller's onEntry, so all existing safety checks still apply. No async work is added inside onEntry (extract-zip does not await it).
  • Refactored throwOnSymlinks to share the new getEntryMode helper and mode constants (behavior unchanged).

packages/zip/README.md: documented the new option and its intended use (trusted temporary extraction of user-supplied archives where the caller must be able to read, move, and remove the extracted tree).

Tests

Added Vitest coverage that builds zips programmatically with archiver (no dependency on the system zip):

  • Default behavior unchanged: a read-only directory stays read-only when the option is omitted.
  • ensureOwnerPermissions: true successfully extracts a zip with a 0555 directory, a nested file, a read-only file (owner write added), and an already-writable file (left untouched).
  • Executable files keep their execute bits while gaining owner write.
  • Inferred directories (trailing slash, no POSIX type bit) get the directory type bit set.
  • Entries with no POSIX mode bits are left untouched.
  • Symlink rejection still throws SYMLINK_NOT_ALLOWED with the option enabled.

pnpm --filter @tryghost/zip test → 21 passing; lint and oxfmt --check clean. extract.js is at 100% line / 98% branch coverage.

Notes for reviewers

  • This change is intentionally opt-in; defaults are preserved.
  • GScan/Ghost can enable ensureOwnerPermissions in a follow-up when validating uploaded theme zips.
  • Only owner permission bits are added — group/world permissions are never widened, and extract-zip masks the on-disk mode to 0o777, so no setuid/setgid/sticky escalation is possible.

- Adds an `ensureOwnerPermissions` option (default `false`) to `extract()`
- When enabled, normalizes extracted entry permissions so the owner can
  always read/move/remove the result: directories gain at least owner rwx
  and files gain at least owner rw, while existing execute/group/world bits
  are preserved
- Fixes archives containing read-only directories (e.g. `dr-xr-xr-x`/`0555`)
  that otherwise fail to extract because nested files cannot be written into
  them
- Normalization mutates only the in-memory entry handed to extract-zip; the
  source zip is never rewritten or chmodded, and it runs after the existing
  size-limit, symlink and filename checks
- Defaults are unchanged for existing callers

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Jun 25, 2026

Copy link
Copy Markdown

Warning

Review limit reached

@sagzy, we couldn't start this review because you've reached your PR review rate limit.

More reviews will be available in 22 minutes and 11 seconds. Learn how PR review limits work.

Your organization has used up its prepaid credits, and credit purchases are no longer available. Enable the review add-on in the billing tab to keep reviews running — you're only billed for reviews past your plan's rate limits ($0.25/file).

⌛ How to resolve this issue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based credits.

🚦 How do rate limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please see our Fair Usage Limits Policy for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 3b644941-7b29-49e3-ae8e-5803b09244fd

📥 Commits

Reviewing files that changed from the base of the PR and between 9ab1ad3 and 95bac4e.

📒 Files selected for processing (3)
  • packages/zip/README.md
  • packages/zip/lib/extract.js
  • packages/zip/test/zip.test.js
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch add-perms-check

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@codecov-commenter

codecov-commenter commented Jun 25, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 97.54%. Comparing base (9ab1ad3) to head (95bac4e).

Additional details and impacted files
@@            Coverage Diff             @@
##             main     #761      +/-   ##
==========================================
- Coverage   98.04%   97.54%   -0.51%     
==========================================
  Files          84        3      -81     
  Lines        2759      122    -2637     
  Branches      506       26     -480     
==========================================
- Hits         2705      119    -2586     
+ Misses         12        1      -11     
+ Partials       42        2      -40     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: eaef85d500

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread packages/zip/lib/extract.js Outdated
sagzy and others added 2 commits June 25, 2026 11:52
…ions

Previously, entries with no POSIX mode bits early-returned from
ensureOwnerPermissions and were handed to extract-zip as mode 0. When the
caller combined `ensureOwnerPermissions: true` with a `defaultDirMode`/
`defaultFileMode` that omits owner bits (e.g. 0o555/0o444), extract-zip
synthesized that owner-less default, so a no-POSIX-mode directory/file could
still be created without owner write — violating the option's guarantee.

Now zero-mode entries resolve the same default extract-zip would apply
(mirroring its getExtractedMode fallback) and then have the owner bits OR-ed
in, so the owner can always read/move/traverse the extracted tree regardless
of the supplied defaults. The default case (no custom defaults) is unchanged.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The JSDoc for `defaultDirMode`, `defaultFileMode`, `onEntry`, and the `limits`
sub-fields omitted the `[ ]` optional markers, so the types generated from the
JSDoc treated them as required. TypeScript consumers (e.g. gscan) could not call
`extract(zip, dest, {ensureOwnerPermissions: true})` without also supplying those
params. They all have defaults or are "if present", so they are now correctly
marked optional.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.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.

3 participants