Bug 707686

Summary: Path traversal to arbitrary files if the current directory is in the permitted paths.
Product: Ghostscript Reporter: zhutyra
Component: Security (public)Assignee: Chris Liddell (chrisl) <chris.liddell>
Status: RESOLVED FIXED    
Severity: normal CC: akhaitov, carnil, cbuissar, dr, jsmeix, ken.sharp, marc.deslauriers, rlescak, robin.watts, sam, till.kamppeter
Priority: P2    
Version: unspecified   
Hardware: PC   
OS: Linux   
Customer: Word Size: ---
Attachments: patch

Description zhutyra 2024-03-24 03:09:38 UTC
Created attachment 25510 [details]
patch

When the `gp_validate_path_len` function validates a path, it distinguishes between absolute and relative paths. In the case of relative paths, it will check the path with and without the current-directory-prefix ("foo" and "./foo").

The problem is that it doesn't take into account paths with a parent-directory-prefix. So a path like "../../foo" is also tested as "./../../foo" and if the current directory "./" is in the permitted paths, it will pass the check and you can access arbitrary files.

The fix is simple, I added a test for a path with the parent directory prefix.
Comment 1 zhutyra 2024-03-24 03:11:29 UTC
Created attachment 25511 [details]
testcase

```
gs -P curdirtravers.ps
gs -I./ curdirtravers.ps
gs --permit-file-XXX=./* curdirtravers.ps
```
Comment 2 Ken Sharp 2024-04-05 08:10:32 UTC
It's been a busy few weeks, and it still hasn't settled down yet, so I'm commenting on all the open bug threads to reassure people that we are working on these. Due to slow responses on one thread it we are unfortunately not yet able to estimate a patch release date.

Because we know that there is at least one person watching/mining our public repositories for security commits we are keeping these in a private repository until a patch release. I'm afraid that means I can't just point to a proposed commit in Gitweb.

The patch for this issue is :

diff --git a/base/gpmisc.c b/base/gpmisc.c
index 59423584a..7d102d386 100644
--- a/base/gpmisc.c
+++ b/base/gpmisc.c
@@ -1042,7 +1042,7 @@ gp_validate_path_len(const gs_memory_t *mem,
                      const uint         len,
                      const char        *mode)
 {
-    char *buffer, *bufferfull;
+    char *buffer, *bufferfull = NULL;
     uint rlen;
     int code = 0;
     const char *cdirstr = gp_file_name_current();
@@ -1097,8 +1097,10 @@ gp_validate_path_len(const gs_memory_t *mem,
             return gs_error_VMerror;
 
         buffer = bufferfull + prefix_len;
-        if (gp_file_name_reduce(path, (uint)len, buffer, &rlen) != gp_combine_success)
-            return gs_error_invalidfileaccess;
+        if (gp_file_name_reduce(path, (uint)len, buffer, &rlen) != gp_combine_success) {
+            code = gs_note_error(gs_error_invalidfileaccess);
+            goto exit;
+        }
         buffer[rlen] = 0;
     }
     while (1) {
@@ -1133,9 +1135,34 @@ gp_validate_path_len(const gs_memory_t *mem,
             code = gs_note_error(gs_error_invalidfileaccess);
         }
         if (code < 0 && prefix_len > 0 && buffer > bufferfull) {
+            uint newlen = rlen + cdirstrl + dirsepstrl;
+            char *newbuffer;
+            int code;
+
             buffer = bufferfull;
             memcpy(buffer, cdirstr, cdirstrl);
             memcpy(buffer + cdirstrl, dirsepstr, dirsepstrl);
+
+            /* We've prepended a './' or similar for the current working directory. We need
+             * to execute file_name_reduce on that, to eliminate any '../' or similar from
+             * the (new) full path.
+             */
+            newbuffer = (char *)gs_alloc_bytes(mem->thread_safe_memory, newlen + 1, "gp_validate_path");
+            if (newbuffer == NULL) {
+                code = gs_note_error(gs_error_VMerror);
+                goto exit;
+            }
+
+            memcpy(newbuffer, buffer, rlen + cdirstrl + dirsepstrl);
+            newbuffer[newlen] = 0x00;
+
+            code = gp_file_name_reduce(newbuffer, (uint)newlen, buffer, &newlen);
+            gs_free_object(mem->thread_safe_memory, newbuffer, "gp_validate_path");
+            if (code != gp_combine_success) {
+                code = gs_note_error(gs_error_invalidfileaccess);
+                goto exit;
+            }
+
             continue;
         }
         else if (code < 0 && cdirstrl > 0 && prefix_len == 0 && buffer == bufferfull) {
@@ -1154,6 +1181,7 @@ gp_validate_path_len(const gs_memory_t *mem,
                                            gs_path_control_flag_is_scratch_file);
     }
 
+exit:
     gs_free_object(mem->thread_safe_memory, bufferfull, "gp_validate_path");
 #ifdef EACCES
     if (code == gs_error_invalidfileaccess)
Comment 3 zhutyra 2024-04-05 10:06:42 UTC
Right now I'm not on a computer where I can compile and test it, but I think it fixes the vulnerability.

But you don't need to reduce the path twice and I don't like that the code is sort of "algorithmically" still wrong.
* It looks at the cwd prefix before the path is reduced. After the reduction, the path is changed so the previous look at the prefix is irrelevant.
* The path variants with and without cwd prefix is still tried on all non-absolute paths.

I would like it more like this
```
if it is a pipe:
    leave the path as is
elif it is a weird pipe-like path:
    reject it (done in the other patch)
else:
    first of all reduce the path
    if and only iff it is a cwd path:
        fiddle with the cwd prefix
```
Comment 4 Chris Liddell (chrisl) 2024-04-29 12:39:45 UTC
Use CVE-2024-33870