Commit 4a9dc1075 for imagemagick.org

commit 4a9dc1075dcad3ab0579e1b37dbe854c882699a5
Author: Dirk Lemstra <dirk@lemstra.org>
Date:   Tue Feb 3 21:09:59 2026 +0100

    Prevent path traversal of paths that are blocked in the security policy (GHSA-8jvj-p28h-9gm7)

diff --git a/MagickCore/module.c b/MagickCore/module.c
index ca5ed0fe9..480dc53bb 100644
--- a/MagickCore/module.c
+++ b/MagickCore/module.c
@@ -595,15 +595,16 @@ static MagickBooleanType GetMagickModulePath(const char *filename,
           (void) ConcatenateMagickString(path,DirectorySeparator,
             MagickPathExtent);
         (void) ConcatenateMagickString(path,filename,MagickPathExtent);
-#if defined(MAGICKCORE_HAVE_REALPATH)
         {
           char
-            resolved_path[PATH_MAX+1];
+            *real_path = realpath_utf8(path);

-          if (realpath(path,resolved_path) != (char *) NULL)
-            (void) CopyMagickString(path,resolved_path,MagickPathExtent);
+          if (real_path != (char *) NULL)
+            {
+              (void) CopyMagickString(path,real_path,MagickPathExtent);
+              real_path=DestroyString(real_path);
+            }
         }
-#endif
         if (IsPathAccessible(path) != MagickFalse)
           {
             module_path=DestroyString(module_path);
diff --git a/MagickCore/policy.c b/MagickCore/policy.c
index 9ef1d6e67..2d96f5dce 100644
--- a/MagickCore/policy.c
+++ b/MagickCore/policy.c
@@ -640,6 +640,9 @@ static MagickBooleanType IsPolicyCacheInstantiated(ExceptionInfo *exception)
 MagickExport MagickBooleanType IsRightsAuthorized(const PolicyDomain domain,
   const PolicyRights rights,const char *pattern)
 {
+  char
+    *real_pattern = (char *) NULL;
+
   const PolicyInfo
     *policy_info;

@@ -647,7 +650,8 @@ MagickExport MagickBooleanType IsRightsAuthorized(const PolicyDomain domain,
     *exception;

   MagickBooleanType
-    authorized;
+    authorized,
+    match;

   ElementInfo
     *p;
@@ -671,22 +675,33 @@ MagickExport MagickBooleanType IsRightsAuthorized(const PolicyDomain domain,
       *policy;

     policy=(const PolicyInfo *) p->value;
-    if ((policy->domain == domain) &&
-        (GlobExpression(pattern,policy->pattern,MagickFalse) != MagickFalse))
+    if (policy->domain == domain)
       {
-        if ((rights & ReadPolicyRights) != 0)
-          authorized=(policy->rights & ReadPolicyRights) != 0 ? MagickTrue :
-            MagickFalse;
-        if ((rights & WritePolicyRights) != 0)
-          authorized=(policy->rights & WritePolicyRights) != 0 ? MagickTrue :
-            MagickFalse;
-        if ((rights & ExecutePolicyRights) != 0)
-          authorized=(policy->rights & ExecutePolicyRights) != 0 ? MagickTrue :
-            MagickFalse;
+        if ((policy->domain == PathPolicyDomain) &&
+            (real_pattern == (const char *) NULL))
+          real_pattern=realpath_utf8(pattern);
+        if (real_pattern != (char*) NULL)
+          match=GlobExpression(real_pattern,policy->pattern,MagickFalse);
+        else
+          match=GlobExpression(pattern,policy->pattern,MagickFalse);
+        if (match != MagickFalse)
+          {
+            if ((rights & ReadPolicyRights) != 0)
+              authorized=(policy->rights & ReadPolicyRights) != 0 ? MagickTrue :
+                MagickFalse;
+            if ((rights & WritePolicyRights) != 0)
+              authorized=(policy->rights & WritePolicyRights) != 0 ?
+                MagickTrue : MagickFalse;
+            if ((rights & ExecutePolicyRights) != 0)
+              authorized=(policy->rights & ExecutePolicyRights) != 0 ?
+                MagickTrue : MagickFalse;
+          }
       }
     p=p->next;
   }
   UnlockSemaphoreInfo(policy_semaphore);
+  if (real_pattern != (char *) NULL)
+    real_pattern=DestroyString(real_pattern);
   return(authorized);
 }

diff --git a/MagickCore/token.c b/MagickCore/token.c
index 4de86135b..84259aea2 100644
--- a/MagickCore/token.c
+++ b/MagickCore/token.c
@@ -518,6 +518,7 @@ MagickExport MagickBooleanType GlobExpression(
         target=DestroyString(target);
         break;
       }
+#if !defined(MAGICKCORE_WINDOWS_SUPPORT) || defined(__CYGWIN__)
       case '\\':
       {
         pattern+=GetUTFOctets(pattern);
@@ -525,6 +526,7 @@ MagickExport MagickBooleanType GlobExpression(
           break;
         magick_fallthrough;
       }
+#endif
       default:
       {
         if (case_insensitive != MagickFalse)
diff --git a/MagickCore/utility-private.h b/MagickCore/utility-private.h
index 444c706ca..d8b68e0e1 100644
--- a/MagickCore/utility-private.h
+++ b/MagickCore/utility-private.h
@@ -267,6 +267,121 @@ static inline FILE *popen_utf8(const char *command,const char *type)
 #endif
 }

+static inline char *realpath_utf8(const char *path)
+{
+#if !defined(MAGICKCORE_WINDOWS_SUPPORT) || defined(__CYGWIN__)
+#if defined(MAGICKCORE_HAVE_REALPATH)
+  return(realpath(path,(char *) NULL));
+#else
+  return(AcquireString(path));
+#endif
+#else
+  char
+    *real_path;
+
+  DWORD
+    final_path_length,
+    full_path_length;
+
+  HANDLE
+    file_handle;
+
+  int
+    length,
+    utf8_length;
+
+  wchar_t
+    *clean_path,
+    *full_path,
+    *wide_path;
+
+  /*
+    Convert UTF-8 to UTF-16.
+  */
+  if (path == (const char *) NULL)
+    return((char *) NULL);
+  length=MultiByteToWideChar(CP_UTF8,0,path,-1,NULL,0);
+  if (length <= 0)
+    return((char *) NULL);
+  wide_path=(wchar_t *) AcquireQuantumMemory(length,sizeof(wchar_t));
+  if (wide_path == (wchar_t *) NULL)
+    return((char *) NULL);
+  MultiByteToWideChar(CP_UTF8,0,path,-1,wide_path,length);
+  /*
+    Normalize syntactically.
+  */
+  full_path_length=GetFullPathNameW(wide_path,0,NULL,NULL);
+  if (full_path_length == 0)
+    {
+      wide_path=(wchar_t *) RelinquishMagickMemory(wide_path);
+      return((char *) NULL);
+    }
+  full_path=(wchar_t *) AcquireQuantumMemory(full_path_length,sizeof(wchar_t));
+  if (full_path == (wchar_t *) NULL)
+    {
+      wide_path=(wchar_t *) RelinquishMagickMemory(wide_path);
+      return((char *) NULL);
+    }
+  GetFullPathNameW(wide_path,full_path_length,full_path,NULL);
+  wide_path=(wchar_t *) RelinquishMagickMemory(wide_path);
+  /*
+    Open the file/directory to resolve symlinks.
+  */
+  file_handle=CreateFileW(full_path,GENERIC_READ,FILE_SHARE_READ |
+    FILE_SHARE_WRITE | FILE_SHARE_DELETE,NULL,OPEN_EXISTING,
+    FILE_FLAG_BACKUP_SEMANTICS,NULL);
+  if (file_handle != INVALID_HANDLE_VALUE)
+    {
+      /*
+        Resolve final canonical path.
+      */
+      final_path_length=GetFinalPathNameByHandleW(file_handle,NULL,0,
+        FILE_NAME_NORMALIZED);
+      if (final_path_length == 0)
+        {
+          CloseHandle(file_handle);
+          full_path=(wchar_t *) RelinquishMagickMemory(full_path);
+          return((char *) NULL);
+        }
+      full_path=(wchar_t *) RelinquishMagickMemory(full_path);
+      full_path=(wchar_t *) AcquireQuantumMemory(final_path_length,
+        sizeof(wchar_t));
+      if (full_path == (wchar_t *) NULL)
+        {
+          CloseHandle(file_handle);
+          return((char *) NULL);
+        }
+      GetFinalPathNameByHandleW(file_handle,full_path,final_path_length,
+        FILE_NAME_NORMALIZED);
+      CloseHandle(file_handle);
+    }
+  /*
+    Remove \\?\ prefix for POSIX-like behavior.
+  */
+  clean_path=full_path;
+  if (wcsncmp(full_path,L"\\\\?\\",4) == 0)
+    clean_path=full_path+4;
+  /*
+    Convert UTF-16 to UTF-8.
+  */
+  utf8_length=WideCharToMultiByte(CP_UTF8,0,clean_path,-1,NULL,0,NULL,NULL);
+  if (utf8_length <= 0)
+    {
+      full_path=(wchar_t *) RelinquishMagickMemory(full_path);
+      return NULL;
+    }
+  real_path=(char *) AcquireQuantumMemory(utf8_length,sizeof(char));
+  if (real_path == (char *) NULL)
+    {
+      full_path=(wchar_t *) RelinquishMagickMemory(full_path);
+      return NULL;
+    }
+  WideCharToMultiByte(CP_UTF8,0,clean_path,-1,real_path,utf8_length,NULL,NULL);
+  full_path=(wchar_t *) RelinquishMagickMemory(full_path);
+  return(real_path);
+#endif
+}
+
 static inline int remove_utf8(const char *path)
 {
 #if !defined(MAGICKCORE_WINDOWS_SUPPORT) || defined(__CYGWIN__)
diff --git a/MagickCore/utility.c b/MagickCore/utility.c
index 5b8f117c3..4e78d2891 100644
--- a/MagickCore/utility.c
+++ b/MagickCore/utility.c
@@ -1042,16 +1042,23 @@ MagickPrivate MagickBooleanType GetExecutionPath(char *path,const size_t extent)
 #if defined(MAGICKCORE_HAVE__NSGETEXECUTABLEPATH)
   {
     char
-      executable_path[PATH_MAX << 1],
-      execution_path[PATH_MAX+1];
+      executable_path[PATH_MAX << 1];

     uint32_t
       length;

     length=sizeof(executable_path);
-    if ((_NSGetExecutablePath(executable_path,&length) == 0) &&
-        (realpath(executable_path,execution_path) != (char *) NULL))
-      (void) CopyMagickString(path,execution_path,extent);
+    if (_NSGetExecutablePath(executable_path,&length) == 0)
+      {
+        char
+          *real_path = realpath_utf8(executable_path);
+
+        if (real_path != (char *) NULL)
+          {
+            (void) CopyMagickString(path,real_path,extent);
+            real_path=DestroyString(real_path);
+          }
+      }
   }
 #endif
 #if defined(MAGICKCORE_HAVE_GETEXECNAME)
@@ -1097,10 +1104,13 @@ MagickPrivate MagickBooleanType GetExecutionPath(char *path,const size_t extent)
     if (count != -1)
       {
         char
-          execution_path[PATH_MAX+1];
+          *real_path = realpath_utf8(program_name);

-        if (realpath(program_name,execution_path) != (char *) NULL)
-          (void) CopyMagickString(path,execution_path,extent);
+        if (real_path != (char *) NULL)
+          {
+            (void) CopyMagickString(path,real_path,extent);
+            real_path=DestroyString(real_path);
+          }
       }
     if (program_name != program_invocation_name)
       program_name=(char *) RelinquishMagickMemory(program_name);
@@ -1882,7 +1892,7 @@ MagickPrivate MagickBooleanType ShredFile(const char *path)
     {
       char
         *property;
-
+
       passes=0;
       property=GetEnvironmentValue("MAGICK_SHRED_PASSES");
       if (property != (char *) NULL)