Hacktoberfest 2026: los issues que los mantenedores marcaron para octubre, abiertos y aptos para principiantes. Explorar issues de Hacktoberfest

Pages send no Cache-Control and rely on PHP's session.cache_limiter, so proxy caches break login or serve member pages

Abierto
#470 1 comentario 0 reacciones 0 asignados Ver en GitHub

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
Tipo de issue
Error
Claridad
Bien especificado
Estado de actividad
Activo
Stack tecnológico
nginx, php
Área
backend, security

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

Impact: High Level: Moderate Privacy GDPR AVG Type: Bug Value: Security

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, Expires or Vary: Cookie, yet each contains a per-visitor CSRF token.
  • Member pages are no-store only because of an ini default that Hypha does not set itself.
  • Static files (serveFile()) send Cache-Control: public, but also Pragma: public and, with a session, Expires: 1981. /images/ and /files/ can carry a Set-Cookie with 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_limiter may not be nocache, for example because of a hosting panel, a shared php.ini or 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-Control leak in the same way, even with the PHP default. Hypha can limit this with Vary: Cookie and 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 ignores Set-Cookie would hand one visitor's CSRF cookie to everyone.

How it was tested

Setup

  • Hypha dev in 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-alpine in front, with an empty cache per run and X-Cache-Status exposed. 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 hyphaSession cookie.
    • D: caches static paths only.
  • 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-store for members;
    • Cache-Control: private, no-cache for anonymous visitors, because the page holds a per-visitor CSRF token;
    • Vary: Cookie for both.
  • Static files: in serveFile(), remove the Pragma and Expires headers of the session limiter, and Vary and Set-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/ETag for 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 (or Vary/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_bypass and proxy_no_cache $hypha_has_session as a second line of defence.
  • Keep PHP's session.cache_limiter at nocache.
  • 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

  1. Lee el issue completo y luego la guía de contribución del proyecto.
  2. Comenta en el issue que vas a ocuparte — evita que dos personas hagan lo mismo.
  3. Haz un fork del repositorio y trabaja en una rama.
  4. Abre un pull request que haga referencia al número del issue.

Más de PlanBCode/hypha

Todos los issues de PlanBCode/hypha

Issues similares

Más issues de JavaScript

Recibe los nuevos issues en tu correo

Un resumen breve de issues de GitHub para principiantes.