fix: prevent symlink traversal during retrieve (partial-delete and writes) @W-24124120@ @W-24138711@ - #1833
fix: prevent symlink traversal during retrieve (partial-delete and writes) @W-24124120@ @W-24138711@#1833WillieRuemmele wants to merge 4 commits into
Conversation
…4124120@ Use lstatSync instead of statSync to reject symlinked content directories from partial-delete processing. Filter out symlinks within content directories before deletion. Add defense-in-depth symlink check in deleteFilePath.
QA InstructionsSetup
Test 1: Symlinked content directory is NOT followed during partial-delete (W-24124120)
Test 2: Normal partial-delete still works (no false positives)
Test 3: Symlinked file blocks retrieve writes (W-24138711)
Test 4: Symlinked directory blocks retrieve writes (W-24138711)
Test 5: Normal retrieve writes work (no false positives)
|
Extract findSymlinkOnPath to shared fileSystemHandler utility and add symlink traversal protection to StandardWriter._write() — covers both the write and delete paths for all metadata types during retrieve.
Resolves conflicts in streams.ts (combined symlink + path traversal imports), streams.test.ts (kept both symlink and path traversal tests), and eslint-suppressions.json (combined _write dangle counts).
| (fr: FileResponseSuccess): FileResponseSuccess => { | ||
| if (fr.filePath) { | ||
| if (isSymlinkSync(fr.filePath)) { | ||
| logger.debug(`Skipping delete of symlink ${fr.filePath} to prevent modification of files outside the project.`); |
There was a problem hiding this comment.
logger.warn might me more appropriate for this.
|
🔴 Partial-delete still deletes outside the project when a bundle ancestor dir is a symlink (W-24124120 not fully closed) The delete path only guards the leaf (the last segment of the path), so the containment boundary can still be escaped by a symlinked ancestor directory. The gap
Neither inspects an ancestor of the content root. Local resolution uses The write-path guard would catch the symlinked ancestor (it walks all segments), but it throws only later in Repro (confirmed against this branch) End-to-end through the real The delete precedes the write abort, so the write-path guard does not protect against this. (I have a failing mocha I can share) Suggested fix Give the delete path the same full-path protection the write path already has. In |
Summary
lstatSyncinstead ofstatSyncwhen checking if component content paths are directories, preventing symlinked content directories from entering the partial-delete flow. Filter out symlinks within content directories before deletion and add defense-in-depth symlink check indeleteFilePath.findSymlinkOnPathcheck inStandardWriter._write()before every file write and delete during retrieve, covering all metadata types. If any path segment between the project root and the output file is a symbolic link, the operation is rejected with a clear error.findSymlinkOnPathfromStaticResourceMetadataTransformertofileSystemHandler.tsso it can be reused across the partial-delete, write, and static resource extraction paths.Security Context
During
sf project retrieve start, two distinct I/O paths followed symbolic links without validation:supportsPartialDeletetypes (e.g. DigitalExperienceBundle), the logic would follow symlinks when enumerating and deleting local-only content. If a content directory was a symlink to an external path, retrieve could delete files outside the Salesforce project.StandardWriterthat writes all retrieved metadata to disk would follow symlinks in the output path. A symlinked file or directory inside the project (e.g. planted in a shared Git repo) could redirect writes to overwrite files outside the workspace.Both attack vectors require a crafted project (e.g. a malicious Git repo with preserved symlinks) that a victim clones and retrieves against.
@W-24124120@ @W-24138711@
Proof of Work
retrieveExtract.test.ts, 3 instreams.test.ts, 4 infileSystemHandler.test.ts)Test plan