View Issue Details

IDProjectCategoryView StatusLast Update
0011697Talerpaivanapublic2026-08-04 23:03
ReporterFlorian Dold Assigned ToChristian Grothoff Resolution deadline
ImplementerReviewer 
Release signed byDeployed by 
VerifierCommunicator 
Data protection authority notification requiredNo  
CSIRT notification requiredNo  
Estimated effort (hours)Actual effort (hours)0 
Fast-trackNo
Work packagenone
PrioritynormalSeveritymajorReproducibilityhave not tried
Status informingResolutionfixed 
Product Versiongit (master) 
Target Version1.6Fixed in Version1.6 
Summary0011697: paivana paywall response cache is unbounded and keyed on client-controlled headers
Description`load_paywall()` in `src/backend/paivana-httpd_templates.c` memoizes the rendered
paywall page in a per-template linked list of `struct ResponseCacheEntry`. The
lookup keys on two request headers taken **verbatim**:

```c
  lang = MHD_lookup_connection_value (conn, MHD_HEADER_KIND,
                                      MHD_HTTP_HEADER_ACCEPT_LANGUAGE);
  ae = MHD_lookup_connection_value (conn, MHD_HEADER_KIND,
                                      MHD_HTTP_HEADER_ACCEPT_ENCODING);
  for (struct ResponseCacheEntry *pos = t->rce_head; NULL != pos; pos = pos->next)
    if ( (eq (lang, pos->lang)) && (eq (ae, pos->ae)) )
      return MHD_queue_response (conn, pos->http_status, pos->paywall);
```

`eq()` is an exact `strcmp`, so every distinct header string is a distinct key.
On a miss the page is rendered and a new entry is appended
(`GNUNET_CONTAINER_DLL_insert (t->rce_head, t->rce_tail, rce)`). There is no
capacity limit, no expiry and no eviction anywhere: entries are freed only in
`PAIVANA_HTTPD_unload_templates()`, i.e. at shutdown.

Both header values are entirely attacker-controlled and are not normalised before
being used as the key. `TALER_TEMPLATING_build()` does its own language
negotiation internally and falls back to English for anything it does not have a
template for, so the cached *bodies* are in the overwhelming majority identical —
the cache stores many copies of the same page under different keys.

The rendered paywall is not small: the English page is ~48 KB
(`Content-Length: 48498` as served). Each unique key therefore costs roughly that
much, retained for the lifetime of the process.

Two further points:

- The paywall is served on the unauthenticated path — no cookie, no payment, no
  rate limit — so this needs nothing but the ability to make HTTP requests.
- `HEAD` requests reach the same code (after the fix for
  https://bugs.taler.net/n/11640) and are the cheapest way to drive it, since the
  response body is never transferred back to the client. The cache entry is still
  created. The defect itself predates that fix and applies identically to `GET`.
Steps To ReproduceWith paivana running behind a template whose `website_regex` covers some URL:

```shell
for i in $(seq 1 20000); do
  curl -s -o /dev/null --head -H "Accept-Language: x-$i" http://HOST/some-page
done
ps -o rss= -C paivana-httpd
```

RSS grows roughly linearly with the number of distinct header values and never
comes back down. ~20k requests are enough to retain on the order of a gigabyte.
Varying `Accept-Encoding` instead has the same effect.
Additional Information## Suggested fix

Any one of:

- Key the cache on the *negotiated* result — the template language actually
  selected plus whether the body was compressed — rather than on the raw header
  strings. That collapses the key space to the handful of combinations that
  produce genuinely different bytes, and fixes the duplication as a side effect.
- Cap the list (LRU, or simply stop inserting past N entries and render
  uncached beyond that).
- Drop the cache; `Cache-Control: public, max-age=300` is already set on the
  response, so intermediaries and browsers absorb most repeat traffic anyway.

The first option looks best: it is the smallest change that makes the cache both
bounded and actually effective.
TagsNo tags attached.

Required notifications

Address or linkRemarkAdded byNotifiedActions

Activities

There are no notes attached to this issue.

Issue History

Date ModifiedUsernameFieldChange
2026-07-28 15:18Florian DoldNew Issue
2026-08-04 23:03Christian GrothoffAssigned To => Christian Grothoff
2026-08-04 23:03Christian GrothoffStatusnew => resolved
2026-08-04 23:03Christian GrothoffResolutionopen => fixed
2026-08-04 23:03Christian GrothoffFixed in Version => 1.6
2026-08-04 23:03Christian GrothoffProduct Version => git (master)
2026-08-04 23:03Christian GrothoffTarget Version => 1.6