Skip to content

fix: enforce size limit on individual files in build_bundle() - #3911

Open
Quratulain-bilal wants to merge 6 commits into
github:mainfrom
Quratulain-bilal:fix/bundle-unbounded-read
Open

Quratulain-bilal wants to merge 6 commits into
github:mainfrom
Quratulain-bilal:fix/bundle-unbounded-read

Conversation

@Quratulain-bilal

Copy link
Copy Markdown
Contributor

Problem

build_bundle() called read_bytes() without any size guard. A single large asset file could exhaust memory.

Fix

Enforce MAX_ZIP_MEMBER_BYTES (10 MiB) limit before reading each file.

Testing

  • Verified normal files are packaged correctly
  • Verified oversized files are rejected

@Quratulain-bilal
Quratulain-bilal requested a review from mnriem as a code owner July 31, 2026 12:57

@mnriem mnriem left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Please resolve conflicts

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.

Pull request overview

Adds per-file size enforcement when building bundle archives.

Changes:

  • Rejects files exceeding the 10 MiB member limit.
  • Adds oversized and boundary-size tests.
Show a summary per file
File Description
src/specify_cli/bundler/services/packager.py Adds file-size validation before archive writes.
tests/unit/test_bundler_packager.py Tests rejection and exact-limit acceptance.

Review details

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

  • Files reviewed: 2/2 changed files
  • Comments generated: 1
  • Review effort level: Balanced

Comment thread src/specify_cli/bundler/services/packager.py Outdated
@mnriem mnriem added the triage-can-wait Verdict: valid and in-scope but deprioritized; held behind the evidence gate label Sep 11, 2026
@mnriem
mnriem requested a balanced review from Copilot September 11, 2026 17:47

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.

🟡 Changes recommended

Rejection can leave a partial archive at the final output path and overwrite a previous valid artifact.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

Review details
  • Files reviewed: 2/2 changed files
  • Comments generated: 1
  • Review effort level: Balanced

Comment thread src/specify_cli/bundler/services/packager.py Outdated
@mnriem

mnriem commented Sep 17, 2026

Copy link
Copy Markdown
Collaborator

Please address Copilot feedback

@Quratulain-bilal

Copy link
Copy Markdown
Contributor Author

Hi @mnriem — I've addressed Copilot's feedback: builds now write to a temporary sibling file first, and the final artifact path is only replaced atomically after every member passes validation. This prevents a partial/corrupt archive from being left at the output path if a file exceeds the size limit. The temp file is cleaned up on any failure. Added est_oversized_file_does_not_corrupt_output and est_temp_file_cleaned_up_on_failure to verify the atomic behavior. All 19 tests pass. Ready for re-review when you get a chance.

@mnriem
mnriem requested a balanced review from Copilot September 23, 2026 12:29
@mnriem

mnriem commented Sep 23, 2026

Copy link
Copy Markdown
Collaborator

Please resolve conflicts

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

Archive permissions and staging-file handling introduce unresolved blocking issues.

Get a fresh assessment by requesting another Copilot review.

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

Open (2)
Resolved since last review (1)

Comment thread src/specify_cli/bundles/packager.py
Comment thread src/specify_cli/bundles/packager.py
@mnriem

mnriem commented Sep 23, 2026

Copy link
Copy Markdown
Collaborator

Please address Copilot feedback and resolve conflicts

Reject files exceeding MAX_ZIP_MEMBER_BYTES (10 MiB) before archiving, bounding the actual read to prevent a TOCTOU bypass of the size check. Build into a temporary sibling file and atomically replace the final artifact only after every member passes validation, so a rejected member never leaves a partial/corrupt archive at the output path; the staging file is cleaned up on any failure.
…lection

mkstemp() creates the staging file 0600 and os.replace() preserves that mode, so every rebuilt artifact became owner-only; chmod the staging file before replacing (preserving an existing artifact's mode on rebuild, otherwise 0644). Also exclude leftover mkstemp staging files (<id>-<version>-<random>.tmp) from _collect_files so a killed build's partial archive is never re-packaged when out_dir is the bundle source tree.

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

Staging-file filtering is incorrect in several cases, and manifest loading remains unbounded.

Get a fresh assessment by requesting another Copilot review.

Review effort: Balanced
Findings: 1 Medium severity

Open (1)
Resolved since last review (2)

Comment thread src/specify_cli/bundles/packager.py
@mnriem

mnriem commented Sep 24, 2026

Copy link
Copy Markdown
Collaborator

Please address Copilot feedback

The member loop's size check runs after BundleManifest.from_file() has
already read bundle.yml in full, so an oversized manifest still reached
the YAML parser. load_yaml() now takes an optional max_bytes and reads
through a bounded byte read, BundleManifest.from_file() forwards it, and
build_bundle() passes MAX_ZIP_MEMBER_BYTES for the manifest.

Regression coverage: an oversized bundle.yml must be rejected at the
manifest read, identified by the manifest's full path in the message.
Before this change that test failed with the member-loop message
("Bundle file bundle.yml exceeds ...") only after the unbounded parse had
already run.

Assisted-by: opencode (model: mimo-v2.6-flash-free, autonomous)

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

Fresh artifacts bypass restrictive umasks, and staging-file filtering can silently omit legitimate assets.

Review effort: Balanced
Findings: 2 Medium severity · 1 Low severity

Open (3)
Resolved since last review (1)

Comment thread src/specify_cli/bundles/packager.py Outdated
Comment thread src/specify_cli/bundles/packager.py Outdated
Comment on lines +139 to +140
content = fh.read(MAX_ZIP_MEMBER_BYTES + 1)
if len(content) > MAX_ZIP_MEMBER_BYTES:
@mnriem

mnriem commented Oct 6, 2026

Copy link
Copy Markdown
Collaborator

Please address Copilot feedback

Addresses 3 Copilot findings:

1. Honor umask for fresh artifacts: use 0666 & ~umask instead of
   unconditional 0644 so restrictive umasks (077) produce private
   0600 files while normal umasks yield 0644.

2. Restrict staging-file filtering to the bundle root: staging files
   are created in the output directory, not arbitrary asset dirs.
   A nested asset like assets/demo-bundle-1.2.0-backup.tmp is now
   correctly packaged.

3. Add bounded-read regression test: simulates a TOCTOU race where
   an asset grows past MAX_ZIP_MEMBER_BYTES between fstat and read.
   Verifies rejection, previous artifact preservation, and temp
   file cleanup.

Tests added:
- test_fresh_artifact_honors_restrictive_umask
- test_nested_staging_like_asset_is_packaged
- test_read_is_bounded_when_asset_grows_after_fstat

Assisted-by: GitHub Copilot (autonomous)
@Quratulain-bilal

Copy link
Copy Markdown
Contributor Author

Hi @mnriem — I've addressed all 3 Copilot findings:

1. Honor umask for fresh artifacts (Medium)
Changed os.chmod(tmp_path, 0o644) to os.chmod(tmp_path, 0o666 & ~umask) so restrictive umasks (077) produce private 0600 files while normal umasks (022) yield the expected 0644. Added \ est_fresh_artifact_honors_restrictive_umask.

2. Restrict staging-file filtering to bundle root (Medium)
Changed the staging filter to only apply when
oot_path == bundle_dir, so legitimate nested assets like \�ssets/demo-bundle-1.2.0-backup.tmp\ are correctly packaged. Added \ est_nested_staging_like_asset_is_packaged.

3. Add coverage for bounded reads after asset growth (Low)
Added \ est_read_is_bounded_when_asset_grows_after_fstat\ that simulates a TOCTOU race — an asset grows past MAX_ZIP_MEMBER_BYTES between fstat and read. Verifies rejection, previous artifact preservation, and temp file cleanup.

All 24 tests pass. Ready for re-review when you get a chance.

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.

🟡 Changes recommended

The staging filter can omit valid assets, and process-wide umask handling introduces a permissions race.

3 open findings
2 resolved since last review
Previously missed (1)

In code that hasn't changed since last review

Low severity Replace invalid characters with em dashes

src/​specify_cli/​bundles/​yamlio.py:46

Replace the two invalid replacement characters with em dashes so this docstring remains readable.

🧠 Review effort: Balanced

Comment on lines +158 to +159
umask = os.umask(0)
os.umask(umask) # restore immediately
Comment on lines +388 to +390
bundle = _make_bundle(tmp_path / "b")
result = build_bundle(bundle, output_dir=tmp_path / "out")
assert stat.S_IMODE(result.artifact_path.stat().st_mode) == 0o644
@mnriem

mnriem commented Oct 12, 2026

Copy link
Copy Markdown
Collaborator

Please address Copilot feedback and resolve conflicts

…read

# Conflicts:
#	src/specify_cli/bundles/manifest.py
@mnriem
mnriem requested a balanced review from Copilot October 13, 2026 11:56

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

triage-can-wait Verdict: valid and in-scope but deprioritized; held behind the evidence gate

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants