diff --git a/src/common.h b/src/common.h
index 38f9958..7c25a70 100644
--- a/src/common.h
+++ b/src/common.h
@@ -680,7 +680,7 @@ char *normalize_filename (idx_t, char const *);
 void normalize_filename_x (char *name);
 void replace_prefix (char **pname, const char *samp, idx_t slen,
 		     const char *repl, idx_t rlen);
-char *tar_savedir (const char *name, bool must_exist);
+char *tar_savedir (const char *name, bool must_exist, bool nofollow);
 
 typedef struct namebuf *namebuf_t;
 namebuf_t namebuf_create (const char *dir);
@@ -788,6 +788,7 @@ struct chdir_id { int err; dev_t st_dev; ino_t st_ino; } chdir_id (void);
 struct fdbase fdbase (char const *);
 struct fdbase fdbase1 (char const *);
 struct fdbase fdbase_escape (char const *, bool);
+struct fdbase fdbase_removal (char const *);
 int open_searchdir (char const *);
 int fdbase_close (int);
 void fdbase_clear (void);
diff --git a/src/incremen.c b/src/incremen.c
index 87e3b13..5ebb94d 100644
--- a/src/incremen.c
+++ b/src/incremen.c
@@ -1647,7 +1647,7 @@ purge_directory (char const *directory_name)
   if (!is_dumpdir (&current_stat_info))
     return;
 
-  current_dir = tar_savedir (directory_name, false);
+  current_dir = tar_savedir (directory_name, false, true);
 
   if (!current_dir)
     /* The directory doesn't exist now.  It'll be created.  In any
diff --git a/src/misc.c b/src/misc.c
index f3fffac..db39b37 100644
--- a/src/misc.c
+++ b/src/misc.c
@@ -32,7 +32,7 @@
 static void namebuf_add_dir (namebuf_t, char const *);
 static char *namebuf_finish (namebuf_t);
 static const char *tar_getcdpath (idx_t);
-static struct fdbase fdbase_opendir (char const *, bool, bool, int);
+static struct fdbase fdbase_opendir (char const *, bool, bool, int, bool);
 
 char const *
 quote_n_colon (int n, char const *arg)
@@ -697,7 +697,7 @@ remove_any_file (const char *file_name, enum remove_option option)
      non-directory.  */
   bool try_unlink_first = cannot_unlink_dir ();
 
-  struct fdbase f = fdbase (file_name);
+  struct fdbase f = fdbase_removal (file_name);
 
   if (try_unlink_first)
     {
@@ -740,7 +740,7 @@ remove_any_file (const char *file_name, enum remove_option option)
 
 	case RECURSIVE_REMOVE_OPTION:
 	  {
-	    char *directory = tar_savedir (file_name, false);
+	    char *directory = tar_savedir (file_name, false, true);
 	    char const *entry;
 	    idx_t entrylen;
 
@@ -764,7 +764,7 @@ remove_any_file (const char *file_name, enum remove_option option)
 	      }
 
 	    free (directory);
-	    return safer_rmdir (file_name, fdbase (file_name)) == 0;
+	    return safer_rmdir (file_name, fdbase_removal (file_name)) == 0;
 	  }
 	}
       break;
@@ -1113,7 +1113,7 @@ chdir_do (idx_t i, bool create)
 	    chdir_do ((i - 1) & ~+one_top_level, false);
 
 	  int oflags = open_searchdir_how.flags & ~O_NOFOLLOW;
-	  fd = fdbase_opendir (curr->name, false, true, oflags).fd;
+	  fd = fdbase_opendir (curr->name, false, true, oflags, false).fd;
 	  if (fd < 0)
 	    {
 	      if (errno == ENOENT)
@@ -1123,7 +1123,8 @@ chdir_do (idx_t i, bool create)
 		      if (!create_dir (curr->name))
 			fatal_exit ();
 		      /* Directory likely exists now; retry.  */
-		      fd = fdbase_opendir (curr->name, false, true, oflags).fd;
+		      fd = fdbase_opendir (curr->name, false, true, oflags,
+					   false).fd;
 		    }
 		  else if (i & one_top_level)
 		    {
@@ -1270,10 +1271,31 @@ fdbase_close (int fd)
 /* Starting from the directory FD, open a subdirectory SUBDIR for search.
    If OFLAGS, open with OFLAGS.  Otherwise, open_searchdir_how
    determines whether SUBDIR can escape FD, i.e., whether it must
-   be at or under FD in the directory hierarchy.  */
+   be at or under FD in the directory hierarchy.
+   If NOFOLLOW, add O_NOFOLLOW, so that a symbolic link is never
+   opened through even if --dereference is in effect.  */
 static int
-open_subdir (int fd, char const *subdir, int oflags)
+open_subdir (int fd, char const *subdir, int oflags, bool nofollow)
 {
+  if (nofollow)
+    {
+      if (oflags)
+	return openat (fd, subdir, oflags | O_NOFOLLOW);
+      /* Compose O_NOFOLLOW into open_searchdir_how rather than
+	 falling back to plain openat, so that any RESOLVE_* flags
+	 stay in effect.  Also require SUBDIR to stay at or under the
+	 search root even if --dereference is in effect: O_NOFOLLOW
+	 protects only the final component, whereas a symbolic link
+	 swapped in at run time as an intermediate component would
+	 otherwise let removals be redirected outside the tree.
+	 Do not add RESOLVE_BENEATH if --absolute-names is in effect,
+	 to match how open_searchdir_how.resolve is initialized.  */
+      struct open_how how = open_searchdir_how;
+      how.flags |= O_NOFOLLOW;
+      if (!absolute_names_option)
+	how.resolve |= RESOLVE_BENEATH;
+      return openat2 (fd, subdir, &how, sizeof how);
+    }
   return
     (oflags
      ? openat (fd, subdir, oflags)
@@ -1289,10 +1311,13 @@ open_subdir (int fd, char const *subdir, int oflags)
    If CHILD, open FILE_NAME; otherwise open FILE_NAME's parent directory.
    If OFLAGS, open the directory with those flags, possibly letting it
    escape from chdir_fd; otherwise, do not let it escape.
+   If NOFOLLOW, never open the directory through a symbolic link,
+   even if --dereference is in effect.
    Return AT_FDCWD if FILE_NAME is relative to the working directory.
    Return BADFD (setting errno) on failure.  */
 static struct fdbase
-fdbase_opendir (char const *file_name, bool alternate, bool child, int oflags)
+fdbase_opendir (char const *file_name, bool alternate, bool child, int oflags,
+		bool nofollow)
 {
   char const *name = file_name;
   int dfd = IS_ABSOLUTE_FILE_NAME (file_name) ? AT_FDCWD : chdir_fd;
@@ -1385,7 +1410,8 @@ fdbase_opendir (char const *file_name, bool alternate, bool child, int oflags)
      open descendant to FD rather than to CHDIR_FD.  */
   bool descendant = old_prefixes_new & chdirmatch;
   int newfd = open_subdir (descendant ? fd : chdir_fd,
-			   &newdir[descendant ? subdirlen : 0], oflags);
+			   &newdir[descendant ? subdirlen : 0], oflags,
+			   nofollow);
   if (newfd < 0)
     return (struct fdbase) { .fd = BADFD, .base = base };
 
@@ -1407,7 +1433,7 @@ struct fdbase
 fdbase_escape (char const *name, bool escape)
 {
   return fdbase_opendir (name, false, false,
-			 escape ? open_searchdir_how.flags : 0);
+			 escape ? open_searchdir_how.flags : 0, false);
 }
 
 /* Return an fd open to NAME's parent directory
@@ -1419,6 +1445,20 @@ fdbase (char const *name)
   return fdbase_escape (name, false);
 }
 
+/* Return an fd open to NAME's parent directory
+   along with the corresponding base name.
+   Do not escape from chdir_fd unless -P is used.
+   Unlike fdbase, never open the directory through a symbolic link,
+   even if --dereference is in effect: use this in removal contexts
+   (e.g., remove_any_file and directory listings that feed it), where
+   following a symlink swapped in at run time would let tar remove
+   files outside the intended tree.  */
+struct fdbase
+fdbase_removal (char const *name)
+{
+  return fdbase_opendir (name, false, false, 0, true);
+}
+
 /* Return an fd open to NAME's parent directory
    along with the corresponding base name.
    Do not escape from chdir_fd unless -P is used.
@@ -1427,7 +1467,7 @@ fdbase (char const *name)
 struct fdbase
 fdbase1 (char const *name)
 {
-  return fdbase_opendir (name, true, false, 0);
+  return fdbase_opendir (name, true, false, 0, false);
 }
 
 /* Return an fd open to NAME.
@@ -1435,7 +1475,8 @@ fdbase1 (char const *name)
 int
 open_searchdir (char const *name)
 {
-  return fdbase_opendir (name, false, true, open_searchdir_how.flags).fd;
+  return fdbase_opendir (name, false, true, open_searchdir_how.flags,
+			 false).fd;
 }
 
 
@@ -1709,17 +1750,24 @@ namebuf_finish (namebuf_t buf)
 /* Return the filenames in directory NAME, relative to the chdir_fd.
    If the directory does not exist, report error if MUST_EXIST is
    true.
+   If NOFOLLOW, never open the directory (nor its parent, when the
+   parent is opened by name) through a symbolic link, even if
+   --dereference is in effect; this is for removal contexts, where
+   following a symlink swapped in at run time would let tar list (and
+   then remove) files outside the intended tree.
 
    Return NULL on errors.
 */
 char *
-tar_savedir (const char *name, bool must_exist)
+tar_savedir (const char *name, bool must_exist, bool nofollow)
 {
   char *ret = NULL;
   DIR *dir = NULL;
-  struct fdbase f = fdbase (name);
+  struct fdbase f = (nofollow ? fdbase_removal (name) : fdbase (name));
   int fd = (f.fd == BADFD ? -1
-	    : openat (f.fd, f.base, open_read_flags | O_DIRECTORY));
+	    : openat (f.fd, f.base,
+		      open_read_flags | O_DIRECTORY
+		      | (nofollow ? O_NOFOLLOW : 0)));
   if (fd < 0)
     {
       if (!must_exist && errno == ENOENT)
diff --git a/src/update.c b/src/update.c
index fa4883b..40b7a52 100644
--- a/src/update.c
+++ b/src/update.c
@@ -140,7 +140,7 @@ update_archive (void)
 		      {
 			char *p;
 			char *dirp = tar_savedir (current_stat_info.file_name,
-						  true);
+						  true, false);
 			if (dirp)
 			  {
 			    namebuf_t nbuf = namebuf_create (current_stat_info.file_name);
