paivana

HTTP paywall reverse proxy
Log | Files | Refs | Submodules | README | LICENSE

commit 4f46991729f7e70790d7d397db932d813d78783b
parent ccd0fb34b4f024eb9651fc9ee26104a67d33e5b0
Author: Christian Grothoff <christian@grothoff.org>
Date:   Thu,  6 Aug 2026 16:08:49 +0200

reject non-canonical URLs

Diffstat:
Msrc/backend/paivana-httpd_daemon.c | 68++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
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")) &&