Pages send no Cache-Control and rely on PHP's session.cache_limiter, so proxy caches break login or serve member pages
Los mantenedores suelen responder en 1 día
Nadie ha tomado este issue todavía.
Evaluación
- Dificultad
- 4/5
- Tiempo estimado
- 3-5 días
- Aptitud para principiantes
- 43/100
Línea de trabajo
Start with the affected entry points listed in the issue: index.php, system/core/HyphaSession.php, system/core/database.php (serveFile()), and system/core/users.php; review the linked logout work in #433 and PR #461 before changing that part. Reproduce the cache behavior using the NGINX scenarios described, including the empty session.cache_limiter variant. Done means generated pages and static files have safe, explicit cache headers, logout renews session and CSRF state, and the listed login and privacy scenarios pass.
Escrito por el modelo de indexación a partir del texto del issue.
Descripción
Summary
Severity: Medium. A common NGINX proxy_cache setup with default PHP settings breaks login for everyone. It caches anonymous pages together with one visitor's CSRF token. Member and private pages are protected only by an implicit PHP ini default. With that default changed, a cache that honours backend headers served member-only and private content to anonymous visitors.
Hypha sends no caching headers for generated HTML. The only cache headers on HTML come from PHP's session.cache_limiter (default nocache). PHP sends them only when session_start() runs, and Hypha starts the session only for visitors who already have a session cookie. As a result:
- Anonymous pages have no
Cache-Control,ExpiresorVary: Cookie, yet each contains a per-visitor CSRF token. - Member pages are
no-storeonly because of an ini default that Hypha does not set itself. - Static files (
serveFile()) sendCache-Control: public, but alsoPragma: publicand, with a session,Expires: 1981./images/and/files/can carry aSet-Cookiewith a new CSRF token.
Affected code (dev @ 5dae13a)
| Location | Behaviour |
|---|---|
system/core/HyphaSession.php:50-55 |
session_start(), and therefore the cache-limiter headers, runs only when a session cookie exists. |
index.php:173 |
The final output sends only Content-Type. |
system/core/document.php:363-369 and system/core/RequestContext.php:196 |
A per-visitor CSRF token in every page and in a cookie. |
system/core/database.php:810-811 (serveFile()) |
Sends Pragma: public and Cache-Control: public, max-age=86400, but does not remove Expires or Set-Cookie. Reached via index.php:59-62 and system/core/pages.php:286-291. |
system/core/users.php:62-65 |
Logout keeps the session id and the CSRF token. |
Impact
Hypha usually runs behind an NGINX reverse proxy, often with proxy_cache or a CDN.
- Login breaks. Take a cache that honours backend headers and gives responses without headers a default lifetime (
proxy_cache_valid 200 10m, a very common line). It stores anonymous pages.- Every later visitor receives another visitor's CSRF token without the matching cookie, so logging in fails with a CSRF error until the entry expires.
- Logged-in members are served the cached anonymous view.
- Member content can leak.
session.cache_limitermay not benocache, for example because of a hosting panel, a sharedphp.inior another application. Member responses then have no cache headers either. The same cache stores pages rendered for a logged-in member and serves them to anonymous visitors and after logout. They contain:- the member's name, the admin buttons and the CSRF token;
- the settings page and edit forms;
- private pages.
- Aggressive caches leak too. Caches configured to ignore
Cache-Controlleak in the same way, even with the PHP default. Hypha can limit this withVary: Cookieand a new session id at logout. - Static images and files are not cached for first-time visitors, because of the
Set-Cookie. A cache that ignoresSet-Cookiewould hand one visitor's CSRF cookie to everyone.
How it was tested
Setup
- Hypha
devin the project's container image (Apache, PHP 8.2), with a copy of a real data set of about 1,330 pages and a test administrator. nginx:1.27-alpinein front, with an empty cache per run andX-Cache-Statusexposed. Four configurations:- A: honours backend headers,
proxy_cache_valid 200 10m, default cache key. - B:
proxy_ignore_headers Cache-Control Expires Set-Cookie. - C: A, plus bypass and no-cache on the
hyphaSessioncookie. - D: caches static paths only.
- A: honours backend headers,
- A raw HTTP client with one cookie jar per visitor, and a browser-like login.
Scenarios
- A member browses the home page, a public page, a private page, settings and an edit view. Then a new anonymous client requests the same URLs. Then the member logs out and revisits.
- The same in reverse order: anonymous visitors first, then the member.
- A variant with
session.cache_limiter = "".
| Hypha | session.cache_limiter |
NGINX | Anonymous got member content | Member content after logout | Foreign CSRF token served | Login |
|---|---|---|---|---|---|---|
| dev | nocache (default) | A | no | no | yes | fails once an anonymous page is cached |
| dev | nocache | B | yes (public, private, settings, edit) | yes | - | ok |
| dev | nocache | C | no | no | yes | fails |
| dev | nocache | D | no | no | no | ok |
| dev | "" |
A | yes (all five URLs, including the private page) | yes | yes | ok |
| patched | nocache | A / B / C / D | no | no | no | ok |
| patched | "" |
A | no | no | no | ok |
Static image through A, two requests without cookies:
| 1st request | 2nd request | |
|---|---|---|
| dev | MISS | MISS |
| patched | MISS | HIT |
Suggested fix
Hypha side
- Generated pages: send explicit headers for every generated response, independent of the session:
Cache-Control: private, no-storefor members;Cache-Control: private, no-cachefor anonymous visitors, because the page holds a per-visitor CSRF token;Vary: Cookiefor both.
- Static files: in
serveFile(), remove thePragmaandExpiresheaders of the session limiter, andVaryandSet-Cookie. - Logout: give the visitor a new session id and a new CSRF token.
Note: this patch is a suggestion and a starting point, not a finished fix. Please review it and adapt it as you see fit.
diff --git a/index.php b/index.php
index 087ea69..fa7cb37 100644
--- a/index.php
+++ b/index.php
@@ -43,6 +43,14 @@
// Build request context
$O_O = new RequestContext($hyphaRequest);
+ // Generated pages contain a per-visitor CSRF token and, for
+ // members, personal and private content, so shared caches
+ // (reverse proxies, CDNs) must never store them. Do not rely on
+ // session_start(), which only sends cache headers when a session
+ // exists. serveFile() overrides this for static files.
+ header('Cache-Control: ' . ($O_O->isUser() ? 'private, no-store' : 'private, no-cache'));
+ header('Vary: Cookie');
+
// called for its side effects of loading the datatypes
hypha_getDataTypes();
diff --git a/system/core/database.php b/system/core/database.php
index eaf45df..c432d44 100644
--- a/system/core/database.php
+++ b/system/core/database.php
@@ -807,7 +807,14 @@
// private.
// TODO: Support ETag or If-Modified-Since to allow
// cheap validation of cached contents?
- header('Pragma: public');
+ // Static files are the same for everyone: drop the
+ // headers of the session cache limiter and any cookie
+ // (e.g. a new CSRF token), so shared caches may store
+ // them.
+ header_remove('Pragma');
+ header_remove('Expires');
+ header_remove('Vary');
+ header_remove('Set-Cookie');
header('Cache-Control: public, max-age=86400');
if ($start === 0 && $end === $filesize) {
diff --git a/system/core/users.php b/system/core/users.php
index 58a6643..2ea922e 100644
--- a/system/core/users.php
+++ b/system/core/users.php
@@ -62,6 +62,11 @@
$sess = $O_O->getSession();
$sess->lockAndReload();
$sess->remove('hyphaLogin');
+ // New session id and CSRF token, so nothing cached for the
+ // logged-in session (e.g. by a cache keyed on cookies) is
+ // reused after logout.
+ $sess->changeSessionId();
+ $O_O->regenerateCsrfToken();
$sess->writeAndUnlock();
return ['redirect', $O_O->getRequest()->getRootUrl() . $hyphaQuery];
}
Possible follow-ups:
Last-Modified/ETagfor static files;- a versioned URL for the theme CSS;
- moving the CSRF token out of the HTML, so anonymous pages can be shared by caches.
The logout change overlaps with #433 (PR #461), which renews the session at logout as well; whichever is merged first, the other drops that part.
NGINX side (operators)
Until the fix is released, cache only static paths and never the HTML:
map $http_cookie $hypha_has_session { default 0; ~*hyphaSession 1; }
location / { proxy_pass http://hypha_backend; }
location ~ ^/(data/themes|system/assets|system/wymeditor|system/bowser|images|files)/ {
proxy_pass http://hypha_backend;
proxy_cache hypha;
proxy_ignore_headers Set-Cookie;
proxy_hide_header Set-Cookie;
proxy_cache_valid 200 1h;
}
Further:
- Never use
proxy_ignore_headers Cache-Control Expires(orVary/Set-Cookie) for Hypha pages. - Bypassing the cache for the session cookie alone is not enough: logins still break.
- After the fix, keep
proxy_cache_bypassandproxy_no_cache $hypha_has_sessionas a second line of defence. - Keep PHP's
session.cache_limiteratnocache. - On CDNs, avoid "cache everything" rules for HTML.
- Lenguaje dominante
- JavaScript
- Estrellas
- 5
- Forks
- 9
- Merge medio
- 19 h 10 min
- PR fusionados (30 d)
- 12
Preparar el entorno
Este proyecto no incluye contenedor de desarrollo, Dockerfile ni guía de contribución, así que la configuración corre por tu cuenta: empieza por su README y consulta nuestra guía para la primera contribución para los pasos generales.
Primeros pasos
- Lee el issue completo y luego la guía de contribución del proyecto.
- Comenta en el issue que vas a ocuparte — evita que dos personas hagan lo mismo.
- Haz un fork del repositorio y trabaja en una rama.
- Abre un pull request que haga referencia al número del issue.
Más de PlanBCode/hypha
-
Component: User interface Impact: Low Level: Easy Pagetype: Peer reviewed article Type: Bug Value: Visitors
Dificultad 2/5 1-3 horas Aptitud para principiantes 75/100
Los mantenedores suelen responder en 1 día
-
Impact: Medium Level: Easy Type: Bug Value: Users
Dificultad 2/5 1-3 horas Aptitud para principiantes 68/100
Los mantenedores suelen responder en 1 día
-
Impact: Low Level: Easy Pagetype: Festival Type: Bug Value: Coders
Dificultad 1/5 Menos de una hora Aptitud para principiantes 82/100
Los mantenedores suelen responder en 1 día
-
Impact: Medium Level: Easy Pagetype: Mailinglist Type: Bug Value: Users
Dificultad 2/5 1-3 horas Aptitud para principiantes 79/100
Los mantenedores suelen responder en 1 día
-
Impact: Low Level: Easy Type: Bug Value: Coders
Dificultad 1/5 1-3 horas Aptitud para principiantes 85/100
PlanBCode/hypha#586 · 1 comentario ·
Los mantenedores suelen responder en 1 día
Todos los issues de PlanBCode/hypha
Issues similares
-
Dificultad 2/5 1-3 horas Aptitud para principiantes 82/100
Los mantenedores suelen responder en 1 día
-
Add: OnTime Sports SD LogoAbiertocheck:failed logos:add
Dificultad 2/5 1-3 horas Aptitud para principiantes 67/100
iptv-org/database#38874 · 1 comentario ·
Los mantenedores suelen responder en 4 días
-
documentation
Dificultad 2/5 1-3 horas Aptitud para principiantes 85/100
githubnext/gh-aw-workshop#5193 ·
Los mantenedores suelen responder en 1 día
-
Dificultad 2/5 1-3 horas Aptitud para principiantes 72/100
Los mantenedores suelen responder en 1 día
-
Dificultad 2/5 1-3 horas Aptitud para principiantes 72/100