Skip to content

Conversation

@nwalters512
Copy link
Contributor

@nwalters512 nwalters512 commented Jan 20, 2026

Given that I was able to write a failing test to demonstrate my issue, I opted to open a PR to facilitate discussion. If you'd rather I close this and open an issue instead, please let me know!

340eb28 ended up being a regression for my application, which relies on being able to un-tar archives that contain symlinks that point within the archive itself. Note that I do not use, and do not want to use, preservePaths: true because of the obvious security implications. To the best of my knowledge, there's no security reason to avoid resolving symlinks that don't refer to other paths outside the archive.

If there's agreement that (a) this is a problem and (b) a reasonable fix would be to permit relative symlinks that don't point outside of the tarball, I'd be happy to add an attempted fix to this PR. My proposal would be to replace this check with one that doesn't just check for the presence of ..:

parts.includes('..') ||

If behaving this way under the default preservePaths: false still isn't acceptable, I'd propose a new option to opt into what should still be safe and correct behavior.

@isaacs
Copy link
Owner

isaacs commented Jan 20, 2026

Oh, I see, so the target is within the tarball cwd, but just includes ... Yeah, that's overly broad, I see. It should check that resolving the linkpath doesn't take it outside the cwd.

@nwalters512
Copy link
Contributor Author

It should check that resolving the linkpath doesn't take it outside the cwd.

Exactly, yeah. I'll put up a proposed fix in this PR - feel free to take it or rework it yourself. Thanks for the quick response.

@nwalters512
Copy link
Contributor Author

nwalters512 commented Jan 20, 2026

@isaacs I updated the implementation and some more of the existing tests in 5b7e937.

@nwalters512 nwalters512 changed the title Add reproduction of bug with valid relative symlink Allow unpacking symlinks that don't escape cwd Jan 20, 2026
src/unpack.ts Outdated
return false
}
// Valid relative symlink that stays within cwd
return true
Copy link
Owner

Choose a reason for hiding this comment

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

Not quite. For example, it could be an absolute path that includes .., like consider what would happen if the linkpath is /../etc/passwd.

I'd add a field === 'path' in the next section, but let it fall through to the absolute strip in either case.

Copy link
Owner

@isaacs isaacs Jan 20, 2026

Choose a reason for hiding this comment

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

I think this fixes it:

diff --git a/src/unpack.ts b/src/unpack.ts
index 8f256d0..51c9488 100644
--- a/src/unpack.ts
+++ b/src/unpack.ts
@@ -275,43 +275,43 @@ export class Unpack extends Parser {
 
     const parts = p.split('/')
 
-    // For linkpath, check if the resolved path escapes cwd rather than
-    // just rejecting any path with '..' - relative symlinks like
-    // '../sibling/file' are valid if they resolve within the cwd.
-    if (field === 'linkpath' && parts.includes('..')) {
-      // Resolve linkpath relative to the entry's directory. `path.posix` is
-      // safe to use because we're operating on tar paths, not a filesystem.
-      const entryDir = path.posix.dirname(entry.path)
-      const resolved = path.posix.normalize(
-        path.posix.join(entryDir, p),
-      )
-      // If the resolved path escapes (starts with ..), reject it
-      if (resolved.startsWith('../') || resolved === '..') {
-        this.warn(
-          'TAR_ENTRY_ERROR',
-          `${field} escapes extraction directory`,
-          {
-            entry,
-            [field]: p,
-          },
-        )
-        return false
-      }
-      // Valid relative symlink that stays within cwd
-      return true
-    }
-
     if (
       parts.includes('..') ||
       /* c8 ignore next */
       (isWindows && /^[a-z]:\.\.$/i.test(parts[0] ?? ''))
     ) {
-      this.warn('TAR_ENTRY_ERROR', `${field} contains '..'`, {
-        entry,
-        [field]: p,
-      })
-      // not ok!
-      return false
+      // For linkpath, check if the resolved path escapes cwd rather than
+      // just rejecting any path with '..' - relative symlinks like
+      // '../sibling/file' are valid if they resolve within the cwd.
+      // For paths, they just simply may not ever use .. at all.
+      if (field === 'path') {
+        this.warn('TAR_ENTRY_ERROR', `${field} contains '..'`, {
+          entry,
+          [field]: p,
+        })
+        // not ok!
+        return false
+      } else {
+        // Resolve linkpath relative to the entry's directory.
+        // `path.posix` is safe to use because we're operating on
+        // tar paths, not a filesystem.
+        const entryDir = path.posix.dirname(entry.path)
+        const resolved = path.posix.normalize(
+          path.posix.join(entryDir, p),
+        )
+        // If the resolved path escapes (starts with ..), reject it
+        if (resolved.startsWith('../') || resolved === '..') {
+          this.warn(
+            'TAR_ENTRY_ERROR',
+            `${field} escapes extraction directory`,
+            {
+              entry,
+              [field]: p,
+            },
+          )
+          return false
+        }
+      }
     }
 
     // strip off the root

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Great catch. I added a failing test in 1f4b85b and fixed it in 46e98fd.

PR-URL: isaacs#450
Credit: @nwalters512
Close: isaacs#450
Reviewed-by: @isaacs

EDIT(@isaacs): fixed for test coverage and to disallow absolute
linkpaths that contain `..`
@isaacs isaacs force-pushed the nwalters512/symlink-dotdot-repro branch from 46e98fd to e9a1ddb Compare January 20, 2026 23:51
@isaacs isaacs closed this in e9a1ddb Jan 20, 2026
@isaacs isaacs merged commit e9a1ddb into isaacs:main Jan 20, 2026
2 of 4 checks passed
@nwalters512 nwalters512 deleted the nwalters512/symlink-dotdot-repro branch January 20, 2026 23:58
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.

2 participants