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.