CVE-2022-48682: Insufficient Information (NVD-CWE-noinfo)
value: In deletefiles in FDUPES before 2.2.0, a TOCTOU race condition allows arbitrary file deletion via a symlink.
CWE description: There is insufficient information about the issue to classify it; details are unkown or unspecified.
Published: 2024-04-26T01:15Z
Repository: https://github.com/adrianlopezroche/fdupes
Language: C
Files changed: Makefile.am, fdupes.c, ncurses-commands.c, removeifnotchanged.c, removeifnotchanged.h
Commit date: 2022-08-16 14:39:49 -0400
Commit message: Call stat() before deleting any file to ensure it's still the same file.

Diff: diff --git a/Makefile.am b/Makefile.am
index 2cf4be5..b134684 100644
--- a/Makefile.am
+++ b/Makefile.am
@@ -15,10 +15,12 @@ fdupes_SOURCES = fdupes.c\
  sigint.h\
  flags.c\
  flags.h\
  confirmmatch.c\
  confirmmatch.h\
+ removeifnotchanged.c\
+ removeifnotchanged.h\
  mbstowcs_escape_invalid.c\
  mbstowcs_escape_invalid.h\
  md5/md5.c\
  md5/md5.h
 dist_man1_MANS = fdupes.1
@@ -57,10 +59,12 @@ fdupes_SOURCES = fdupes.c\
  sigint.h\
  flags.c\
  flags.h\
  confirmmatch.c\
  confirmmatch.h\
+ removeifnotchanged.c\
+ removeifnotchanged.h\
  mbstowcs_escape_invalid.c\
  mbstowcs_escape_invalid.h\
  positive_wcwidth.c\
  positive_wcwidth.h\
  md5/md5.c\
diff --git a/fdupes.c b/fdupes.c
index 1b1e442..cfc91c2 100644
--- a/fdupes.c
+++ b/fdupes.c
@@ -47,10 +47,11 @@
 #include "confirmmatch.h"
 #include "errormsg.h"
 #include "log.h"
 #include "sigint.h"
 #include "flags.h"
+#include "removeifnotchanged.h"
 
 long long minsize = -1;
 long long maxsize = -1;
 
 typedef enum {
@@ -802,10 +803,11 @@ void deletefiles(file_t *files, int prompt, FILE *tty, char *logfile)
   struct log_info *loginfo;
   int log_error;
   FILE *file1;
   FILE *file2;
   int ismatch;
+  char *errorstring;
 
   curfile = files;
   
   while (curfile) {
     if (curfile->hasdupes) {
@@ -1003,19 +1005,19 @@ void deletefiles(file_t *files, int prompt, FILE *tty, char *logfile)
     {
       ismatch = 1;
     }
 
     if (ismatch) {
-      if (remove(dupelist[x]->d_name) == 0) {
+      if (removeifnotchanged(dupelist[x], &errorstring) == 0) {
         printf("   [-] %s\n", dupelist[x]->d_name);
 
         if (loginfo)
           log_file_deleted(loginfo, dupelist[x]->d_name);
       }
       else {
         printf("   [!] %s ", dupelist[x]->d_name);
-        printf("-- unable to delete file!\n");
+        printf("-- unable to delete file: %s!\n", errorstring);
 
         if (loginfo)
           log_file_remaining(loginfo, dupelist[x]->d_name);
       }
     }
@@ -1128,10 +1130,11 @@ void registerpair(file_t **matchlist, file_t *newmatch,
 void deletesuccessor(file_t **existing, file_t *duplicate, int matchconfirmed,
       int (*comparef)(file_t *f1, file_t *f2), struct log_info *loginfo)
 {
   file_t *to_keep;
   file_t *to_delete;
+  char *errorstring;
 
   if (comparef(duplicate, *existing) >= 0)
   {
     to_keep = *existing;
     to_delete = duplicate;
@@ -1154,18 +1157,18 @@ void deletesuccessor(file_t **existing, file_t *duplicate, int matchconfirmed,
   if (loginfo)
     log_file_remaining(loginfo, to_keep->d_name);
 
   if (matchconfirmed)
   {
-    if (remove(to_delete->d_name) == 0) {
+    if (removeifnotchanged(to_delete, &errorstring) == 0) {
       printf("   [-] %s\n", to_delete->d_name);
 
       if (loginfo)
         log_file_deleted(loginfo, to_delete->d_name);
     } else {
       printf("   [!] %s ", to_delete->d_name);
-      printf("-- unable to delete file!\n");
+      printf("-- unable to delete file: %s!\n", errorstring);
 
       if (loginfo)
         log_file_remaining(loginfo, to_delete->d_name);
     }
   }
diff --git a/ncurses-commands.c b/ncurses-commands.c
index 7199f34..c8f401a 100644
--- a/ncurses-commands.c
+++ b/ncurses-commands.c
@@ -27,10 +27,11 @@
 #include "confirmmatch.h"
 #include "errormsg.h"
 #include "wcs.h"
 #include "mbstowcs_escape_invalid.h"
 #include "log.h"
+#include "removeifnotchanged.h"
 #include <wchar.h>
 #include <pcre2.h>
 
 void set_file_action(struct groupfile *file, int new_action, size_t *deletion_tally);
 
@@ -778,11 +779,11 @@ int cmd_prune(struct filegroup *groups, int groupcount, wchar_t *commandargument
           else
           {
             ismatch = 1;
           }
 
-          if (ismatch && remove(groups[g].files[f].file->d_name) == 0)
+          if (ismatch && removeifnotchanged(groups[g].files[f].file, 0) == 0)
           {
             set_file_action(&groups[g].files[f], FILEACTION_DELIST, deletiontally);
 
             deletedbytes += groups[g].files[f].file->size;
             ++totaldeleted;
diff --git a/removeifnotchanged.c b/removeifnotchanged.c
new file mode 100644
index 0000000..c018b5b
--- /dev/null
+++ b/removeifnotchanged.c
@@ -0,0 +1,41 @@
+#include "removeifnotchanged.h"
+#include <errno.h>
+#include <string.h>
+#include <stdio.h>
+
+int removeifnotchanged(const file_t *file, char **errorstring)
+{
+  int result;
+  struct stat st;
+
+  static char *filechanged = "File contents changed during processing";
+  static char *unknownerror = "Unknown error";
+
+  stat(file->d_name, &st);
+
+  if (file->device != st.st_dev ||
+      file->inode != st.st_ino ||
+      file->ctime != st.st_ctime ||
+      file->mtime != st.st_mtime ||
+      file->size != st.st_size)
+  {
+    if (errorstring != 0)
+        *errorstring = filechanged;
+
+    return -2;
+  }
+  else
+  {
+    result = remove(file->d_name);
+
+    if (result != 0 && errorstring != 0)
+    {
+      *errorstring = strerror(errno);
+
+      if (*errorstring == 0)
+        *errorstring = unknownerror;
+    }
+
+    return result;
+  }
+}
diff --git a/removeifnotchanged.h b/removeifnotchanged.h
new file mode 100644
index 0000000..c8da7b5
--- /dev/null
+++ b/removeifnotchanged.h
@@ -0,0 +1,8 @@
+#ifndef REMOVEIFNOTCHANGED_H
+#define REMOVEIFNOTCHANGED_H
+
+#include "fdupes.h"
+
+int removeifnotchanged(const file_t *file, char **errorstring);
+
+#endif
\ No newline at end of file

Vulnerable code:       if (remove(dupelist[x]->d_name) == 0) {
        printf("-- unable to delete file!\n");
    if (remove(to_delete->d_name) == 0) {
      printf("-- unable to delete file!\n");
          if (ismatch && remove(groups[g].files[f].file->d_name) == 0)
Fixed code:  removeifnotchanged.c\
 removeifnotchanged.h\
 removeifnotchanged.c\
 removeifnotchanged.h\
#include "removeifnotchanged.h"
  char *errorstring;
      if (removeifnotchanged(dupelist[x], &errorstring) == 0) {
        printf("-- unable to delete file: %s!\n", errorstring);
  char *errorstring;
    if (removeifnotchanged(to_delete, &errorstring) == 0) {
      printf("-- unable to delete file: %s!\n", errorstring);
#include "removeifnotchanged.h"
          if (ismatch && removeifnotchanged(groups[g].files[f].file, 0) == 0)
#include "removeifnotchanged.h"
#include <errno.h>
#include <string.h>
#include <stdio.h>

int removeifnotchanged(const file_t *file, char **errorstring)
{
  int result;
  struct stat st;

  static char *filechanged = "File contents changed during processing";
  static char *unknownerror = "Unknown error";

  stat(file->d_name, &st);

  if (file->device != st.st_dev ||
      file->inode != st.st_ino ||
      file->ctime != st.st_ctime ||
      file->mtime != st.st_mtime ||
      file->size != st.st_size)
  {
    if (errorstring != 0)
        *errorstring = filechanged;

    return -2;
  }
  else
  {
    result = remove(file->d_name);

    if (result != 0 && errorstring != 0)
    {
      *errorstring = strerror(errno);

      if (*errorstring == 0)
        *errorstring = unknownerror;
    }

    return result;
  }
}
#ifndef REMOVEIFNOTCHANGED_H
#define REMOVEIFNOTCHANGED_H

#include "fdupes.h"

int removeifnotchanged(const file_t *file, char **errorstring);

#endif