This is an automated email from the git hooks/post-receive script.

git pushed a commit to branch fix/file-url-percent-decode
in repository terminology.

View the commit online.

commit 8f309dff880854020a7f37e66551f78ba9be4a7f
Author: Cedric BAIL <[email protected]>
AuthorDate: Thu Apr 30 13:23:43 2026 -0600

    termio: percent-decode file:// URLs before opening
    
    Closes a long-standing TODO at termio.c:710. file:// URIs with
    percent-encoded characters (e.g., spaces as %20) were passed
    verbatim to the file-opening logic, causing the handler to look
    for files literally named with %20 in their filenames.
    
    Add uri_percent_decode_inplace following RFC 3986 §2.1, called right
    after the file:// prefix is stripped. Malformed sequences pass
    through unchanged (no crash on incomplete or non-hex %XX).
    
    Security: %00 truncates the path to prevent NUL-byte injection
    path-confusion (file:///etc/passwd%00.safe.txt would otherwise
    silently open /etc/passwd via path truncation at NUL).
    
    Note: this branch decodes percent-encoded shell metacharacters
    (%60, $24%28, etc.) to their literal forms. The escaping in the
    downstream shell-out path (ecore_file_escape_name) is known to be
    incomplete (does not escape backtick). The fix/shell-injection-user-helpers
    branch addresses that separately. Recommend landing both branches
    together; landing this one alone exposes shell-injection via
    percent-encoded payloads.
    
    Example:
      Before: file:///home/u/My%20Docs → opens '/home/u/My%20Docs'
      After:  file:///home/u/My%20Docs → opens '/home/u/My Docs'
---
 src/bin/meson.build             |   5 +-
 src/bin/termio.c                |   8 +++-
 src/bin/tytest.c                |   1 +
 src/bin/tytest_percent_decode.c | 104 ++++++++++++++++++++++++++++++++++++++++
 src/bin/unit_tests.h            |   1 +
 src/bin/uri_decode.c            |  50 +++++++++++++++++++
 src/bin/uri_decode.h            |  14 ++++++
 7 files changed, 181 insertions(+), 2 deletions(-)

diff --git a/src/bin/meson.build b/src/bin/meson.build
index f12f5d7a..67e112d3 100644
--- a/src/bin/meson.build
+++ b/src/bin/meson.build
@@ -39,7 +39,8 @@ terminology_sources = ['private.h',
                        'extns.c', 'extns.h',
                        'gravatar.c', 'gravatar.h',
                        'tty_keys.h',
-                       'sb.c', 'sb.h']
+                       'sb.c', 'sb.h',
+                       'uri_decode.c', 'uri_decode.h']
 
 tybg_sources = ['tycommon.c', 'tycommon.h', 'tybg.c']
 tyalpha_sources = ['tycommon.c', 'tycommon.h', 'tyalpha.c']
@@ -83,7 +84,9 @@ tytest_sources = ['termptyesc.c', 'termptyesc.h',
                   'theme.h',
                   'md5.c', 'md5.h',
                   'unit_tests.h',
+                  'uri_decode.c', 'uri_decode.h',
                   'tytest_common.c', 'tytest_common.h',
+                  'tytest_percent_decode.c',
                   'tytest.c', 'tytest.h']
 
 executable('terminology',
diff --git a/src/bin/termio.c b/src/bin/termio.c
index baad9d0d..9a10901b 100644
--- a/src/bin/termio.c
+++ b/src/bin/termio.c
@@ -1,4 +1,5 @@
 #include "private.h"
+#include "uri_decode.h"
 
 #include <Elementary.h>
 #include <Elementary_Cursor.h>
@@ -707,7 +708,7 @@ termio_link_get(const Evas_Object *obj,
      {
         if (casestartswith(link, "file://"))
           {
-             // TODO: decode string: %XX -> char
+             char *decoded;
              link = link + sizeof("file://") - 1;
              /* Handle cases where / is omitted: file://HOSTNAME/home/ */
              if (link[0] != '/')
@@ -716,6 +717,11 @@ termio_link_get(const Evas_Object *obj,
                   if (!link)
                     return NULL;
                }
+             /* Percent-decode the path per RFC 3986 §2.1
+              * (e.g., file:///home/u/My%20Docs → /home/u/My Docs) */
+             decoded = strdup(link);
+             if (decoded) uri_percent_decode_inplace(decoded);
+             return decoded;
           }
      }
    return strdup(link);
diff --git a/src/bin/tytest.c b/src/bin/tytest.c
index 02903830..2e61b9b2 100644
--- a/src/bin/tytest.c
+++ b/src/bin/tytest.c
@@ -42,6 +42,7 @@ static struct {
        { "color_parse_css_hsl", tytest_color_parse_css_hsl},
        { "extn_matching", tytest_extn_matching},
        { "base64", tytest_base64},
+       { "percent_decode", tytest_percent_decode},
        { NULL, NULL},
 };
 
diff --git a/src/bin/tytest_percent_decode.c b/src/bin/tytest_percent_decode.c
new file mode 100644
index 00000000..f9a19140
--- /dev/null
+++ b/src/bin/tytest_percent_decode.c
@@ -0,0 +1,104 @@
+#if defined(BINARY_TYTEST)
+
+#include <assert.h>
+#include <string.h>
+
+#include "uri_decode.h"
+#include "unit_tests.h"
+
+int
+tytest_percent_decode(void)
+{
+   char buf[256];
+
+   /* 1. Empty string */
+   strcpy(buf, "");
+   uri_percent_decode_inplace(buf);
+   assert(strcmp(buf, "") == 0);
+
+   /* 2. No percent sequences */
+   strcpy(buf, "hello");
+   uri_percent_decode_inplace(buf);
+   assert(strcmp(buf, "hello") == 0);
+
+   /* 3. Single %20 (space) */
+   strcpy(buf, "a%20b");
+   uri_percent_decode_inplace(buf);
+   assert(strcmp(buf, "a b") == 0);
+
+   /* 4. Multiple %20 */
+   strcpy(buf, "a%20b%20c");
+   uri_percent_decode_inplace(buf);
+   assert(strcmp(buf, "a b c") == 0);
+
+   /* 5. UTF-8 bytes (%C3%A9 = é) */
+   strcpy(buf, "r%C3%A9sum%C3%A9");
+   uri_percent_decode_inplace(buf);
+   assert(strcmp(buf, "résumé") == 0);
+
+   /* 6. Lowercase hex */
+   strcpy(buf, "%2f");
+   uri_percent_decode_inplace(buf);
+   assert(strcmp(buf, "/") == 0);
+
+   /* 7. Uppercase hex */
+   strcpy(buf, "%2F");
+   uri_percent_decode_inplace(buf);
+   assert(strcmp(buf, "/") == 0);
+
+   /* 8. Mixed case */
+   strcpy(buf, "%2A%2a");
+   uri_percent_decode_inplace(buf);
+   assert(strcmp(buf, "**") == 0);
+
+   /* 9. Malformed: % at end of string */
+   strcpy(buf, "a%");
+   uri_percent_decode_inplace(buf);
+   assert(strcmp(buf, "a%") == 0);
+
+   /* 10. Malformed: %X at end (only one hex digit) */
+   strcpy(buf, "a%5");
+   uri_percent_decode_inplace(buf);
+   assert(strcmp(buf, "a%5") == 0);
+
+   /* 11. Malformed: non-hex digit */
+   strcpy(buf, "a%GG");
+   uri_percent_decode_inplace(buf);
+   assert(strcmp(buf, "a%GG") == 0);
+
+   /* 12. % followed by another % then valid hex.
+    *     The first % passes through; %20 decodes. */
+   strcpy(buf, "%%20");
+   uri_percent_decode_inplace(buf);
+   assert(strcmp(buf, "% ") == 0);
+
+   /* 13. Just % alone (no following bytes) */
+   strcpy(buf, "%");
+   uri_percent_decode_inplace(buf);
+   assert(strcmp(buf, "%") == 0);
+
+   /* 15. %00 NUL byte: truncates at the embedded NUL.
+    *     Prevents path-confusion attack:
+    *     file:///etc/passwd%00.safe.txt → /etc/passwd */
+   strcpy(buf, "/etc/passwd%00.safe.txt");
+   uri_percent_decode_inplace(buf);
+   assert(strcmp(buf, "/etc/passwd") == 0);
+   assert(strlen(buf) == 11);
+
+   /* 15b. %00 at start of input: returns empty string */
+   strcpy(buf, "%00abc");
+   uri_percent_decode_inplace(buf);
+   assert(strcmp(buf, "") == 0);
+
+   /* 15c. %00 alone: empty string */
+   strcpy(buf, "%00");
+   uri_percent_decode_inplace(buf);
+   assert(strcmp(buf, "") == 0);
+
+   /* 16. NULL input doesn't crash */
+   uri_percent_decode_inplace(NULL);
+
+   return 0;
+}
+
+#endif
diff --git a/src/bin/unit_tests.h b/src/bin/unit_tests.h
index ed3ddd94..27182df8 100644
--- a/src/bin/unit_tests.h
+++ b/src/bin/unit_tests.h
@@ -19,5 +19,6 @@ int tytest_color_parse_css_rgb(void);
 int tytest_color_parse_css_hsl(void);
 int tytest_extn_matching(void);
 int tytest_base64(void);
+int tytest_percent_decode(void);
 
 #endif
diff --git a/src/bin/uri_decode.c b/src/bin/uri_decode.c
new file mode 100644
index 00000000..3d1ac982
--- /dev/null
+++ b/src/bin/uri_decode.c
@@ -0,0 +1,50 @@
+#include "uri_decode.h"
+
+void
+uri_percent_decode_inplace(char *s)
+{
+   char *r;  /* read cursor */
+   char *w;  /* write cursor */
+
+   if (!s) return;
+
+   for (r = w = s; *r; )
+     {
+        if (r[0] == '%' && r[1] && r[2])
+          {
+             int hi = -1, lo = -1;
+             char c1 = r[1], c2 = r[2];
+
+             if      (c1 >= '0' && c1 <= '9') hi = c1 - '0';
+             else if (c1 >= 'A' && c1 <= 'F') hi = c1 - 'A' + 10;
+             else if (c1 >= 'a' && c1 <= 'f') hi = c1 - 'a' + 10;
+
+             if      (c2 >= '0' && c2 <= '9') lo = c2 - '0';
+             else if (c2 >= 'A' && c2 <= 'F') lo = c2 - 'A' + 10;
+             else if (c2 >= 'a' && c2 <= 'f') lo = c2 - 'a' + 10;
+
+             if (hi >= 0 && lo >= 0)
+               {
+                  char decoded = (char)((hi << 4) | lo);
+                  if (decoded == '\0')
+                    {
+                       /* %00 NUL byte: truncate here.
+                        *
+                        * A NUL mid-path silently truncates at all downstream
+                        * strlen() / open(2) calls, enabling path-confusion attacks:
+                        *   file:///etc/passwd%00.safe.txt  → opens /etc/passwd
+                        *
+                        * RFC 3986 §2.2 reserves NUL for transport, not paths.
+                        * Truncation here matches what the OS would silently do
+                        * anyway, but makes the intent explicit and auditable. */
+                       break;
+                    }
+                  *w++ = decoded;
+                  r += 3;
+                  continue;
+               }
+          }
+        *w++ = *r++;
+     }
+   *w = '\0';
+}
diff --git a/src/bin/uri_decode.h b/src/bin/uri_decode.h
new file mode 100644
index 00000000..d2adfd81
--- /dev/null
+++ b/src/bin/uri_decode.h
@@ -0,0 +1,14 @@
+#ifndef TERMINOLOGY_URI_DECODE_H_
+#define TERMINOLOGY_URI_DECODE_H_
+
+/* Percent-decode a URI string in-place. Decodes %XX sequences to
+ * their byte value. Malformed sequences (incomplete or non-hex)
+ * pass through unchanged.
+ *
+ * Per RFC 3986 §2.1. Used to convert file:// URIs to plain paths.
+ *
+ * Safe to call on NULL (no-op).
+ */
+void uri_percent_decode_inplace(char *s);
+
+#endif

-- 
To stop receiving notification emails like this one, please contact
the administrator of this repository.

Reply via email to