commit 89883a45580779f5ae7f510d8e567ff056682761
parent 3e741b0e9f467efad354ce16379f9a1e85d4c504
Author: Christian Grothoff <christian@grothoff.org>
Date: Tue, 4 Aug 2026 16:00:36 +0200
do not forward our cookie upstream
Diffstat:
5 files changed, 139 insertions(+), 2 deletions(-)
diff --git a/src/backend/paivana-httpd_cookie.c b/src/backend/paivana-httpd_cookie.c
@@ -184,7 +184,7 @@ PAIVANA_HTTPD_compute_cookie (struct GNUNET_TIME_Timestamp cur_time,
*end = '\0';
GNUNET_asprintf (
&res,
- "Paivana-Cookie=%llu-%s; %sPath=%s; Max-Age=%llu; HttpOnly;",
+ PAIVANA_COOKIE_NAME "=%llu-%s; %sPath=%s; Max-Age=%llu; HttpOnly;",
(unsigned long long) (cur_time.abs_time.abs_value_us / 1000LLU / 1000LLU),
cstr,
use_https
diff --git a/src/backend/paivana-httpd_cookie.h b/src/backend/paivana-httpd_cookie.h
@@ -30,6 +30,13 @@
#include <gnunet/gnunet_util_lib.h>
/**
+ * Name of the cookie in which we hand the access token to the
+ * client. It is a credential for *paivana*, not for the site we
+ * proxy, and thus must never be relayed upstream.
+ */
+#define PAIVANA_COOKIE_NAME "Paivana-Cookie"
+
+/**
* Secret for the cookie generation.
*/
extern struct GNUNET_HashCode paivana_secret;
diff --git a/src/backend/paivana-httpd_daemon.c b/src/backend/paivana-httpd_daemon.c
@@ -198,7 +198,7 @@ create_response (void *cls,
website = GNUNET_buffer_reap_str (&buf);
cookie = MHD_lookup_connection_value (con,
MHD_COOKIE_KIND,
- "Paivana-Cookie");
+ PAIVANA_COOKIE_NAME);
if (NULL != cookie)
{
void *ca = NULL;
diff --git a/src/backend/paivana-httpd_reverse.c b/src/backend/paivana-httpd_reverse.c
@@ -32,6 +32,7 @@
#include <gnunet/gnunet_curl_lib.h>
#include <taler/taler_mhd_lib.h>
#include "paivana-httpd.h"
+#include "paivana-httpd_cookie.h"
#include "paivana-httpd_reverse.h"
@@ -896,6 +897,80 @@ build_host_header (const char *url)
}
+/**
+ * Remove our own access cookie from a client-supplied `Cookie:`
+ * header value.
+ *
+ * #PAIVANA_COOKIE_NAME carries the token that proves payment to
+ * *this* proxy; relaying it would hand the origin a credential it
+ * has no business seeing and that it could replay against us. Every
+ * other cookie-pair is the origin's own and is passed through
+ * untouched, in the order and spelling the client used.
+ *
+ * The name comparison is case-insensitive because that is how MHD
+ * matches it when we look the cookie up: anything MHD would accept
+ * as our cookie must also be stripped here.
+ *
+ * @param value raw value of the client's `Cookie:` header
+ * @return the remaining cookie string, or NULL if nothing is left to
+ * forward; to be freed by the caller
+ */
+static char *
+strip_paivana_cookie (const char *value)
+{
+ struct GNUNET_Buffer buf = { 0 };
+ const char *pos = value;
+ bool empty = true;
+
+ while ('\0' != *pos)
+ {
+ const char *start = pos;
+ const char *end = strchrnul (pos,
+ ';');
+ const char *nend;
+
+ pos = ('\0' == *end) ? end : end + 1;
+ /* trim the optional whitespace around the cookie-pair */
+ while ( (start < end) &&
+ ( (' ' == *start) || ('\t' == *start) ) )
+ start++;
+ while ( (end > start) &&
+ ( (' ' == end[-1]) || ('\t' == end[-1]) ) )
+ end--;
+ if (start == end)
+ continue; /* empty element, e.g. from a stray ';' */
+ /* the cookie-name runs up to the first '=' (RFC 6265 §4.2.1) */
+ nend = memchr (start,
+ '=',
+ end - start);
+ if (NULL == nend)
+ nend = end;
+ if ( (strlen (PAIVANA_COOKIE_NAME) == (size_t) (nend - start)) &&
+ (0 == strncasecmp (start,
+ PAIVANA_COOKIE_NAME,
+ nend - start)) )
+ {
+ GNUNET_log (GNUNET_ERROR_TYPE_DEBUG,
+ "Not forwarding our own access cookie upstream\n");
+ continue;
+ }
+ if (! empty)
+ GNUNET_buffer_write_str (&buf,
+ "; ");
+ empty = false;
+ GNUNET_buffer_write (&buf,
+ start,
+ end - start);
+ }
+ if (empty)
+ {
+ GNUNET_buffer_clear (&buf);
+ return NULL;
+ }
+ return GNUNET_buffer_reap_str (&buf);
+}
+
+
/* ************** main loop of cURL interaction ************* */
@@ -916,6 +991,7 @@ con_val_iter (void *cls,
const char *value)
{
struct HttpRequest *hr = cls;
+ char *cookies = NULL;
char *hdr;
(void) kind;
@@ -966,10 +1042,20 @@ con_val_iter (void *cls,
twice. */
return MHD_YES;
}
+ if (0 == strcasecmp (MHD_HTTP_HEADER_COOKIE,
+ key))
+ {
+ /* Our access cookie is for us, not for the origin. */
+ cookies = strip_paivana_cookie (value);
+ if (NULL == cookies)
+ return MHD_YES; /* it was the only cookie: no header to forward */
+ value = cookies;
+ }
GNUNET_asprintf (&hdr,
"%s: %s",
key,
value);
+ GNUNET_free (cookies);
GNUNET_log (GNUNET_ERROR_TYPE_DEBUG,
"Adding header `%s' to HTTP request\n",
hdr);
diff --git a/src/tests/test_reverse_proxy.sh b/src/tests/test_reverse_proxy.sh
@@ -401,6 +401,50 @@ function run_battery() {
fail "upstream did not see X-Test: dingbat-42"
ok
+ # Our access cookie is a credential for paivana itself; the
+ # origin must never see it, while the cookies that are genuinely
+ # the origin's have to survive verbatim. The name match is
+ # case-insensitive (that is how MHD looks it up) but exact: names
+ # that merely contain it are somebody else's cookies.
+ msg "[$label] Paivana-Cookie is stripped from the forwarded Cookie"
+ curl -sS \
+ -H 'Cookie: sid=alpha;paivana-cookie=1234-secret; theme=dark' \
+ -o "$TMPDIR/body" "$(PAIVANA_URL /echo-headers)" 2>"$TMPDIR/err" \
+ || fail "curl: $(cat "$TMPDIR/err")"
+ if grep -qi '^cookie:.*paivana-cookie' "$TMPDIR/body";
+ then
+ fail "access cookie leaked upstream: $(grep -i '^cookie:' "$TMPDIR/body")"
+ fi
+ grep -qi '^cookie:.*sid=alpha' "$TMPDIR/body" || \
+ fail "client cookie sid=alpha was dropped"
+ grep -qi '^cookie:.*theme=dark' "$TMPDIR/body" || \
+ fail "client cookie theme=dark was dropped"
+ ok
+
+ # If the access cookie was the only one, no Cookie header at all
+ # should reach the origin -- not an empty one.
+ msg "[$label] lone Paivana-Cookie leaves no Cookie header"
+ curl -sS -H 'Cookie: Paivana-Cookie=1234-secret' \
+ -o "$TMPDIR/body" "$(PAIVANA_URL /echo-headers)" 2>"$TMPDIR/err" \
+ || fail "curl: $(cat "$TMPDIR/err")"
+ if grep -qi '^cookie:' "$TMPDIR/body";
+ then
+ fail "unexpected Cookie header upstream: $(grep -i '^cookie:' "$TMPDIR/body")"
+ fi
+ ok
+
+ # Cookies whose name merely embeds ours are not ours.
+ msg "[$label] cookies named like ours are not over-stripped"
+ curl -sS \
+ -H 'Cookie: Paivana-Cookie-2=keep; XPaivana-Cookie=keep2' \
+ -o "$TMPDIR/body" "$(PAIVANA_URL /echo-headers)" 2>"$TMPDIR/err" \
+ || fail "curl: $(cat "$TMPDIR/err")"
+ grep -qi '^cookie:.*paivana-cookie-2=keep' "$TMPDIR/body" || \
+ fail "Paivana-Cookie-2 was incorrectly stripped"
+ grep -qi '^cookie:.*xpaivana-cookie=keep2' "$TMPDIR/body" || \
+ fail "XPaivana-Cookie was incorrectly stripped"
+ ok
+
# RFC 9110 §7.6.1, response direction: headers named in the
# *upstream's* Connection header are equally hop-by-hop and must
# not be relayed to the client. The upstream emits one such