paivana

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

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:
Msrc/backend/paivana-httpd_cookie.c | 2+-
Msrc/backend/paivana-httpd_cookie.h | 7+++++++
Msrc/backend/paivana-httpd_daemon.c | 2+-
Msrc/backend/paivana-httpd_reverse.c | 86+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Msrc/tests/test_reverse_proxy.sh | 44++++++++++++++++++++++++++++++++++++++++++++
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