test(integrations/client/packages): batch 2-4 unit coverage + zip-slip guard extraction

Batch 2-4 of the test-strategy rollout. Test-only except one minimal,
behaviour-preserving extraction in file.utils.ts. All suites green:
server 82 suites/836+1todo, editor-ext 86, mcp 270, client (new files) 86.

integrations (server):
- file.utils.ts: extract pure `isEntryPathSafe(entryName, targetDir)` from
  extractZipInternal so the zip-slip/path-traversal guard is unit-testable;
  call site rerouted, behaviour identical (only a warn-message string merged).
- file.utils.zip-safety.spec.ts: traversal/strip/__MACOSX/prefix-confusion
  cases (mutation-resistant: fails if containment loses the path.sep).
- import-formatter / import.utils / table-utils / export utils / import.service
  extractTitleAndRemoveHeading: pure import/export transforms, Notion/XWiki
  formatting, table colspan widths (idempotent), slug/link rewriting.

client:
- safeRedirectPath: open-redirect guard, every reject branch independently.
- buildChatMarkdown (fence anti-breakout), label-colors, normalize-label,
  share tree build, page URL builders, notification time-grouping (fake clock).

packages:
- editor-ext: deriveFootnoteId golden table, parseHtmlEmbedHeight crafted
  values, orphan footnote extraction.
- mcp: deriveFootnoteId parity (drift guard vs editor-ext), applyTextEdits
  idempotency + cross-block replaceAll, diffDocs/summarizeChange on reorder.

Reviewed (APPROVE): extraction behaviour-preserving, assertions mutation-resistant.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
claude_code
2026-06-21 18:22:15 +03:00
parent f8e8ada581
commit 0b2af34029
20 changed files with 2495 additions and 17 deletions
@@ -30,6 +30,52 @@ export function getFileTaskFolderPath(
}
}
/**
* Pure path-safety decision for a single ZIP entry (zip-slip / path-traversal guard).
*
* Reproduces exactly the inline check previously embedded in `extractZipInternal`:
* 1. Strip any leading slashes from the entry name.
* 2. Reject names that fail `yauzl.validateFileName` (e.g. backslashes,
* relative `..` segments, drive letters).
* 3. Reject `__MACOSX/` metadata entries.
* 4. Resolve the entry against the target directory and require it to stay
* strictly inside `targetDir` using a `targetResolved + path.sep` prefix check
* (the trailing separator prevents sibling-directory prefix confusion, e.g.
* `/tmp/x` must not match `/tmp/x-evil`).
*
* @param entryName The decoded (UTF-8) entry file name from the archive.
* @param targetDir Directory the archive is being extracted into.
* @returns `{ safe }` and, when safe, the resolved absolute path of the entry.
*/
export function isEntryPathSafe(
entryName: string,
targetDir: string,
): { safe: boolean; resolved?: string } {
// Strip leading slashes so absolute-looking entries cannot escape the target.
const safe = entryName.replace(/^\/+/, '');
const validationError = yauzl.validateFileName(safe);
if (validationError) {
return { safe: false };
}
// Skip macOS resource-fork metadata entries.
if (safe.startsWith('__MACOSX/')) {
return { safe: false };
}
const fullPath = path.join(targetDir, safe);
const resolved = path.resolve(fullPath);
const targetResolved = path.resolve(targetDir);
// Containment check: resolved path must live strictly inside the target dir.
if (!resolved.startsWith(targetResolved + path.sep)) {
return { safe: false };
}
return { safe: true, resolved };
}
/**
* Extracts a ZIP archive.
*/
@@ -103,29 +149,15 @@ function extractZipInternal(
const name = entry.fileName.toString('utf8');
const safe = name.replace(/^\/+/, '');
const validationError = yauzl.validateFileName(safe);
if (validationError) {
console.warn(`Skipping invalid entry (${validationError})`);
zipfile.readEntry();
return;
}
if (safe.startsWith('__MACOSX/')) {
// Zip-slip / path-traversal guard (see isEntryPathSafe).
if (!isEntryPathSafe(name, target).safe) {
console.warn(`Skipping unsafe entry: ${safe}`);
zipfile.readEntry();
return;
}
const fullPath = path.join(target, safe);
const resolved = path.resolve(fullPath);
const targetResolved = path.resolve(target);
if (!resolved.startsWith(targetResolved + path.sep)) {
console.warn(`Skipping entry (path outside target): ${safe}`);
zipfile.readEntry();
return;
}
// Handle directories
if (/\/$/.test(name)) {
try {