Skip to content

fix(exporters): prevent zip-slip via asset path traversal#72

Merged
hqhq1025 merged 2 commits intomainfrom
wt/loop-fix-zip-slip
Apr 19, 2026
Merged

fix(exporters): prevent zip-slip via asset path traversal#72
hqhq1025 merged 2 commits intomainfrom
wt/loop-fix-zip-slip

Conversation

@hqhq1025
Copy link
Copy Markdown
Collaborator

Summary

  • exportZip joined caller-supplied asset.path directly onto the staging dir, so paths like ../escape.txt or assets/../../x would write outside the staging directory and end up at escaping locations inside the archive (zip-slip).
  • Resolve the target path and verify it stays inside the staging dir; throw EXPORTER_ZIP_UNSAFE_PATH otherwise. Re-throw CodesignError from outer catch so the wrapper does not mask the explicit code.
  • Drop a dead typeof asset.content === 'string' ? a : a ternary while here.

Test plan

  • pnpm --filter @open-codesign/exporters test -- --run zip.test (4/4 pass, includes new traversal cases)

PRINCIPLES checks

  • Compatibility: green (additive validation, error code is new)
  • Upgradeability: green
  • No bloat: green (no deps)
  • Elegance: green

`exportZip` previously joined caller-supplied `asset.path` straight onto
the staging dir, so an asset like `../escape.txt` (or `assets/../../x`)
could write outside the staging area and embed escaping paths in the
archive. Resolve the target and verify it stays under the staging dir;
throw EXPORTER_ZIP_UNSAFE_PATH otherwise. Drop dead `typeof === 'string'
? a : a` ternary while here.

Tests: add traversal cases; existing tests still pass.
Copy link
Copy Markdown
Contributor

@github-actions github-actions Bot left a comment

Choose a reason for hiding this comment

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

Findings

  • [Blocker] Backslash-based traversal can still bypass ZIP entry validation on Windows-style paths — the guard validates the resolved staging path, but safeRel is written to the archive unchanged. Inputs like assets\..\..\escape.txt can remain inside staging on POSIX while still becoming traversal-like ZIP entry names for some extractors, so zip-slip risk is not fully closed. Evidence: packages/exporters/src/zip.ts:75, packages/exporters/src/zip.ts:86
    Suggested fix:
    const safeRel = asset.path.replace(/^\/+/, '');
    const normalizedRel = path.posix.normalize(safeRel.replace(/\\/g, '/'));
    
    if (
      !normalizedRel ||
      normalizedRel === '.' ||
      normalizedRel.startsWith('../') ||
      normalizedRel.includes('/../') ||
      normalizedRel.startsWith('/')
    ) {
      throw new CodesignError(
        `ZIP export rejected unsafe asset path: ${asset.path}`,
        'EXPORTER_ZIP_UNSAFE_PATH',
      );
    }
    
    const localPath = path.resolve(stagingDir, normalizedRel);
    // ...keep resolved-path containment check...
    zip.addFile(localPath, normalizedRel);

Summary

  • Review mode: initial
  • 1 blocker found in latest diff: ZIP path traversal mitigation is incomplete for backslash-based paths.
  • Required docs docs/VISION.md and docs/PRINCIPLES.md: Not found in repo/docs.

Testing

  • Not run (automation)
  • Add a Vitest case for assets\\..\\..\\escape.txt expecting EXPORTER_ZIP_UNSAFE_PATH.

open-codesign Bot

Comment thread packages/exporters/src/zip.ts Outdated
if (opts.assets) {
const stagingResolved = path.resolve(stagingDir);
for (const asset of opts.assets) {
const safeRel = asset.path.replace(/^\/+/, '');
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.

asset.path is normalized only for leading /, but backslashes are left as-is. On Windows-style paths, inputs like assets\\..\\..\\escape.txt can still become traversal-like archive entry names because zip.addFile(..., safeRel) uses the unsanitized entry path. Normalize separators to / and reject any .. segments in the archive entry path before writing to disk and adding to ZIP.

ZIP entries authored on Windows can carry `..\..\etc\passwd` style
backslash separators. On POSIX, `path.resolve` treats `\` as a literal
character, so the previous containment check on the resolved path missed
this vector. Normalize backslashes to forward slashes before resolving so
the relative-path guard sees the real traversal.
Copy link
Copy Markdown
Contributor

@github-actions github-actions Bot left a comment

Choose a reason for hiding this comment

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

Findings

  • None.

Summary

  • Review mode: follow-up after new commits
  • No issues found in the latest diff at this head.
  • Residual risk/testing gap: automated checks were not run in this review environment; assessment is based on static diff inspection only.
  • docs/VISION.md and docs/PRINCIPLES.md: Not found in repo/docs.

Testing

  • Not run (automation)

open-codesign Bot

@hqhq1025 hqhq1025 merged commit 61a1d5f into main Apr 19, 2026
5 of 6 checks passed
@hqhq1025 hqhq1025 deleted the wt/loop-fix-zip-slip branch April 19, 2026 06:15
@hqhq1025 hqhq1025 mentioned this pull request Apr 19, 2026
4 tasks
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.

1 participant