commit 4f46991729f7e70790d7d397db932d813d78783b
parent ccd0fb34b4f024eb9651fc9ee26104a67d33e5b0
Author: Christian Grothoff <christian@grothoff.org>
Date: Thu, 6 Aug 2026 16:08:49 +0200
reject non-canonical URLs
Diffstat:
1 file changed, 68 insertions(+), 0 deletions(-)
diff --git a/src/backend/paivana-httpd_daemon.c b/src/backend/paivana-httpd_daemon.c
@@ -79,6 +79,63 @@ static bool have_daemons;
/**
+ * Is the request target something we may decide about and then forward
+ * unchanged?
+ *
+ * Two strings describe one request here, and they are not the same:
+ * @a raw is the target exactly as it arrived on the request line --
+ * that is what we hand to libcurl -- while @a url is MHD's
+ * percent-decoded, query-stripped path, and that is what the WHITELIST
+ * expression and the templates' expressions are matched against.
+ * Neither is normalized, but libcurl normalizes the URL we give it
+ * (CURLOPT_PATH_AS_IS is not set), so "/assets/../premium/x" matches a
+ * WHITELIST of "/assets/.*" and is then fetched as "/premium/x".
+ * Rather than decide about one resource and fetch another, refuse
+ * anything not already in normal form; browsers normalize before
+ * sending, so only hand-built requests are affected.
+ *
+ * Non-origin-form targets are refused for a second reason: such a
+ * target -- an absolute-form one, or merely one starting with "@" --
+ * is concatenated onto DESTINATION_BASE_URL, where it can re-parse as
+ * userinfo (RFC 3986 section 3.2.1) and let the client choose the host
+ * we connect to.
+ *
+ * @param raw request target as it arrived on the request line
+ * @param url percent-decoded path MHD gives the handler
+ * @return true if the request may proceed
+ */
+static bool
+canonical_request_target (const char *raw,
+ const char *url)
+{
+ const char *p;
+
+ if ( ('/' != raw[0]) ||
+ ('/' != url[0]) )
+ return false; /* not origin-form; RFC 9112 section 3.2.1 */
+ /* Reject ".", ".." and empty segments: RFC 3986 section 5.2.4's
+ remove_dot_segments is exactly what libcurl would apply behind our
+ back, after we have already decided. */
+ for (p = url; NULL != p; p = strchr (p + 1, '/'))
+ {
+ const char *seg = p + 1;
+ const char *end = strchr (seg, '/');
+ size_t len = (NULL == end) ? strlen (seg) : (size_t) (end - seg);
+
+ if (0 == len)
+ return ('\0' == *seg); /* a trailing '/' is fine, "//" is not */
+ if ( ( (1 == len) &&
+ ('.' == seg[0]) ) ||
+ ( (2 == len) &&
+ ('.' == seg[0]) &&
+ ('.' == seg[1]) ) )
+ return false;
+ }
+ return true;
+}
+
+
+/**
* Main MHD callback for handling requests.
*
* @param cls unused
@@ -122,6 +179,17 @@ create_response (void *cls,
memset (&buf,
0,
sizeof (buf));
+ if (! canonical_request_target (rc->url,
+ url))
+ {
+ GNUNET_break_op (0);
+ return TALER_MHD_reply_with_error (
+ rc->connection,
+ MHD_HTTP_BAD_REQUEST,
+ TALER_EC_GENERIC_PARAMETER_MALFORMED,
+ "request-target must be in origin-form and free of '.', '..'"
+ " and empty path segments");
+ }
if ( (! rc->is_paivana) &&
(0 == strcmp (url,
"/.well-known/paivana")) &&