VYPR
High severity8.8NVD Advisory· Published Jun 10, 2026

CVE-2026-44693

CVE-2026-44693

Description

Pi-hole FTL versions 6.0-6.6.0 have a race condition in HTTP session management, allowing unauthenticated local network attackers to hijack admin sessions.

AI Insight

LLM-synthesized narrative grounded in this CVE's description and references.

Pi-hole FTL versions 6.0-6.6.0 have a race condition in HTTP session management, allowing unauthenticated local network attackers to hijack admin sessions.

Vulnerability

Pi-hole FTL, the core engine for Pi-hole's ad blocking, contains a race condition vulnerability in its HTTP session management subsystem. This issue, introduced in version 6.0 with the rewrite of the embedded CivetWeb server, affects versions 6.0 through 6.6.0. The vulnerability stems from a global character buffer, pi_hole_extra_headers, used to pass session IDs without proper synchronization, leading to potential data corruption when multiple threads access it concurrently [1].

Exploitation

An unauthenticated attacker on the local network can exploit this vulnerability by sending concurrent requests to public API endpoints. This triggers a race window between the thread writing a session ID to the shared buffer and another thread reading from it. By carefully timing these requests, an attacker can intercept a Set-Cookie header intended for an active administrator session, thereby gaining unauthorized administrative access without needing any credentials [1].

Impact

Successful exploitation of this vulnerability allows an unauthenticated attacker to hijack an active administrator session. This grants the attacker full administrative privileges and control over the Pi-hole instance, enabling them to modify settings, disable blocking, or potentially redirect network traffic through their controlled instance [1].

Mitigation

This vulnerability has been fixed in Pi-hole FTL version 6.6.1, released on June 10, 2026. Users are strongly advised to update to version 6.6.1 or later to patch this security flaw. No workarounds are specified, and the vulnerability is not listed as being part of the Known Exploited Vulnerabilities (KEV) catalog [2].

AI Insight generated on Jun 10, 2026. Synthesized from this CVE's description and the cited reference URLs; citations are validated against the source bundle.

Affected products

1

Patches

1
2e9c11dedb56

Merge pull request #2835 from pi-hole/fix/api-thread-safety

https://github.com/pi-hole/FTLDominikApr 6, 2026via body-scan-shorthand
10 files changed · +67 24
  • patch/civetweb/0001-Add-FTL-URI-rewriting-changes-to-CivetWeb.patch+1 1 modified
    @@ -24,4 +24,4 @@ index e71dfedc..2ad76693 100644
     +
      // Buffer used for additional "Set-Cookie" headers
      #define PIHOLE_HEADERS_MAXLEN 1024
    - extern char pi_hole_extra_headers[PIHOLE_HEADERS_MAXLEN];
    + extern _Thread_local char pi_hole_extra_headers[PIHOLE_HEADERS_MAXLEN];
    
  • patch/civetweb/0001-Add-mbedTLS-debug-logging-hook.patch+1 1 modified
    @@ -12,7 +12,7 @@ index 2ad76693..52724199 100644
     +
      // Buffer used for additional "Set-Cookie" headers
      #define PIHOLE_HEADERS_MAXLEN 1024
    - extern char pi_hole_extra_headers[PIHOLE_HEADERS_MAXLEN];
    + extern _Thread_local char pi_hole_extra_headers[PIHOLE_HEADERS_MAXLEN];
     diff --git a/src/webserver/civetweb/mod_mbedtls.inl b/src/webserver/civetweb/mod_mbedtls.inl
     index e72685f4..00b9280a 100644
     --- a/src/webserver/civetweb/mod_mbedtls.inl
    
  • patch/civetweb/0001-add-pihole-mods.patch+1 1 modified
    @@ -81,7 +81,7 @@ index 7ea45fb2..f879ff3e 100644
     +
     +// Buffer used for additional "Set-Cookie" headers
     +#define PIHOLE_HEADERS_MAXLEN 1024
    -+extern char pi_hole_extra_headers[PIHOLE_HEADERS_MAXLEN];
    ++extern _Thread_local char pi_hole_extra_headers[PIHOLE_HEADERS_MAXLEN];
     +/********************************************************************************************/
     +
      
    
  • src/api/api.c+1 1 modified
    @@ -129,7 +129,7 @@ int api_handler(struct mg_connection *conn, void *ignored)
     		double_time(),
     		{ false, NULL, NULL, NULL, 0u },
     		{ false },
    -		NULL,
    +		{ 0 },
     		{ API_FLAG_NONE, 0 }
     	};
     
    
  • src/api/auth.c+52 13 modified
    @@ -27,9 +27,31 @@
     #include "database/session-table.h"
     // FTLDBerror()
     #include "database/common.h"
    +// pthread_mutex_t
    +#include <pthread.h>
     
     static uint16_t max_sessions = 0;
     static struct session *auth_data = NULL;
    +// Mutex to protect concurrent access to auth_data from civetweb worker threads
    +static pthread_mutex_t auth_lock = PTHREAD_MUTEX_INITIALIZER;
    +
    +// RAII-style auto-unlock: the mutex is released when the guard variable goes
    +// out of scope or on any return (including hidden returns in JSON macros).
    +// Use AUTOUNLOCK() to release early; the cleanup then becomes a no-op.
    +static inline void auto_unlock(pthread_mutex_t **mtx) {
    +	if(*mtx) pthread_mutex_unlock(*mtx);
    +}
    +#define AUTOLOCK(m) \
    +	pthread_mutex_lock(m); \
    +	__attribute__((cleanup(auto_unlock))) pthread_mutex_t *_alock = (m)
    +// Like AUTOLOCK, but only locks when cond is true. This must be a single
    +// declaration statement (not "if(cond) AUTOLOCK(m)") because AUTOLOCK expands
    +// to two statements and the cleanup variable must live at function scope —
    +// scoping it to an if body would trigger immediate unlock.
    +#define AUTOLOCK_IF(m, cond) \
    +	__attribute__((cleanup(auto_unlock))) pthread_mutex_t *_alock = \
    +		(cond) ? (pthread_mutex_lock(m), (m)) : NULL
    +#define AUTOUNLOCK() do { pthread_mutex_unlock(_alock); _alock = NULL; } while(0)
     
     static void add_request_info(struct ftl_conn *api, const char *csrf)
     {
    @@ -211,6 +233,7 @@ int check_client_auth(struct ftl_conn *api, const bool is_api)
     	}
     
     	bool expired = false;
    +	AUTOLOCK(&auth_lock);
     	for(unsigned int i = 0; i < max_sessions; i++)
     	{
     		if(auth_data[i].used &&
    @@ -271,7 +294,10 @@ int check_client_auth(struct ftl_conn *api, const bool is_api)
     	}
     
     	api->user_id = user_id;
    -	api->session = &auth_data[user_id];
    +	// Copy session so downstream handlers can read api->session without the
    +	// lock, note that this is a valid struct copy in C (no memcpy needed)
    +	api->session = auth_data[user_id];
    +	AUTOUNLOCK();
     
     	api->message = "correct password";
     	return user_id;
    @@ -281,6 +307,7 @@ static int get_all_sessions(struct ftl_conn *api, cJSON *json)
     {
     	const time_t now = time(NULL);
     	cJSON *sessions = JSON_NEW_ARRAY();
    +	AUTOLOCK(&auth_lock);
     	for(int i = 0; i < max_sessions; i++)
     	{
     		if(!auth_data[i].used)
    @@ -296,19 +323,20 @@ static int get_all_sessions(struct ftl_conn *api, cJSON *json)
     		JSON_ADD_NUMBER_TO_OBJECT(session, "login_at", auth_data[i].login_at);
     		JSON_ADD_NUMBER_TO_OBJECT(session, "last_active", auth_data[i].valid_until - config.webserver.session.timeout.v.ui);
     		JSON_ADD_NUMBER_TO_OBJECT(session, "valid_until", auth_data[i].valid_until);
    -		JSON_REF_STR_IN_OBJECT(session, "remote_addr", auth_data[i].remote_addr);
    +		JSON_COPY_STR_TO_OBJECT(session, "remote_addr", auth_data[i].remote_addr);
     		if(auth_data[i].user_agent[0] != '\0')
    -			JSON_REF_STR_IN_OBJECT(session, "user_agent", auth_data[i].user_agent);
    +			JSON_COPY_STR_TO_OBJECT(session, "user_agent", auth_data[i].user_agent);
     		else
     			JSON_ADD_NULL_TO_OBJECT(session, "user_agent");
     		if(auth_data[i].x_forwarded_for[0] != '\0')
    -			JSON_REF_STR_IN_OBJECT(session, "x_forwarded_for", auth_data[i].x_forwarded_for);
    +			JSON_COPY_STR_TO_OBJECT(session, "x_forwarded_for", auth_data[i].x_forwarded_for);
     		else
     			JSON_ADD_NULL_TO_OBJECT(session, "x_forwarded_for");
     		JSON_ADD_BOOL_TO_OBJECT(session, "app", auth_data[i].app);
     		JSON_ADD_BOOL_TO_OBJECT(session, "cli", auth_data[i].cli);
     		JSON_ADD_ITEM_TO_ARRAY(sessions, session);
     	}
    +	AUTOUNLOCK();
     	JSON_ADD_ITEM_TO_OBJECT(json, "sessions", sessions);
     	return 0;
     }
    @@ -330,17 +358,19 @@ static int get_session_object(struct ftl_conn *api, cJSON *json, const int user_
     	}
     
     	// Valid session
    +	AUTOLOCK(&auth_lock);
     	if(user_id > API_AUTH_UNAUTHORIZED && auth_data[user_id].used)
     	{
     		JSON_ADD_BOOL_TO_OBJECT(session, "valid", true);
     		JSON_ADD_BOOL_TO_OBJECT(session, "totp", strlen(config.webserver.api.totp_secret.v.s) > 0);
    -		JSON_REF_STR_IN_OBJECT(session, "sid", auth_data[user_id].sid);
    -		JSON_REF_STR_IN_OBJECT(session, "csrf", auth_data[user_id].csrf);
    +		JSON_COPY_STR_TO_OBJECT(session, "sid", auth_data[user_id].sid);
    +		JSON_COPY_STR_TO_OBJECT(session, "csrf", auth_data[user_id].csrf);
     		JSON_ADD_NUMBER_TO_OBJECT(session, "validity", auth_data[user_id].valid_until - now);
     		JSON_REF_STR_IN_OBJECT(session, "message", api->message);
     		JSON_ADD_ITEM_TO_OBJECT(json, "session", session);
     		return 0;
     	}
    +	AUTOUNLOCK();
     
     	// No valid session
     	JSON_ADD_BOOL_TO_OBJECT(session, "valid", false);
    @@ -352,8 +382,12 @@ static int get_session_object(struct ftl_conn *api, cJSON *json, const int user_
     	return 0;
     }
     
    -static bool delete_session(const int user_id)
    +// Deletes the session of the given user ID. If is_locked is false, we ensure
    +// auth_lock; otherwise, the lock is already held by the caller
    +static bool delete_session(const int user_id, const bool is_locked)
     {
    +	AUTOLOCK_IF(&auth_lock, !is_locked);
    +
     	// Skip if nothing to be done here
     	if(user_id < 0 || user_id >= max_sessions)
     		return false;
    @@ -368,7 +402,7 @@ static bool delete_session(const int user_id)
     
     void delete_all_sessions(void)
     {
    -	// Zero out all sessions without looping
    +	AUTOLOCK(&auth_lock);
     	memset(auth_data, 0, max_sessions*sizeof(*auth_data));
     }
     
    @@ -379,12 +413,14 @@ static int send_api_auth_status(struct ftl_conn *api, const int user_id, const t
     		log_debug(DEBUG_API, "API Auth status: OK");
     
     		// Ten minutes validity
    +		AUTOLOCK(&auth_lock);
     		if(snprintf(pi_hole_extra_headers, sizeof(pi_hole_extra_headers),
     		            FTL_SET_COOKIE,
     		            auth_data[user_id].sid, config.webserver.session.timeout.d.ui) < 0)
     		{
     			return send_json_error(api, 500, "internal_error", "Internal server error", NULL);
     		}
    +		AUTOUNLOCK();
     
     		cJSON *json = JSON_NEW_OBJECT();
     		get_session_object(api, json, user_id, now);
    @@ -399,7 +435,7 @@ static int send_api_auth_status(struct ftl_conn *api, const int user_id, const t
     			strncpy(pi_hole_extra_headers, FTL_DELETE_COOKIE, sizeof(pi_hole_extra_headers));
     
     			// Revoke client authentication. This slot can be used by a new client afterwards.
    -			const int code = delete_session(user_id) ? 204 : 404;
    +			const int code = delete_session(user_id, false) ? 204 : 404;
     
     			// Send empty reply with appropriate HTTP status code
     			send_http_code(api, NULL, code, "");
    @@ -579,6 +615,7 @@ int api_auth(struct ftl_conn *api)
     		}
     
     		// Find unused authentication slot
    +		AUTOLOCK(&auth_lock);
     		for(unsigned int i = 0; i < max_sessions; i++)
     		{
     			// Expired slow, mark as unused
    @@ -587,7 +624,7 @@ int api_auth(struct ftl_conn *api)
     			{
     				log_debug(DEBUG_API, "API: Session of client %u (%s) expired, freeing...",
     				          i, auth_data[i].remote_addr);
    -				delete_session(i);
    +				delete_session(i, true);
     			}
     
     			// Found unused authentication slot (might have been freed before)
    @@ -647,6 +684,7 @@ int api_auth(struct ftl_conn *api)
     					user_id, timestr, auth_data[user_id].remote_addr,
     					empty_password ? "empty password" : "correct response");
     		}
    +		AUTOUNLOCK();
     		if(user_id == API_AUTH_UNAUTHORIZED)
     		{
     			log_warn("No free API seats available (webserver.api.max_sessions = %u), not authenticating client",
    @@ -703,12 +741,13 @@ int api_auth_session_delete(struct ftl_conn *api)
     	if(uid <= API_AUTH_UNAUTHORIZED || uid >= max_sessions)
     		return send_json_error(api, 400, "bad_request", "Session ID out of bounds", NULL);
     
    -	// Check if session is used
    +	// Check if session is used and delete it
    +	AUTOLOCK(&auth_lock);
     	if(!auth_data[uid].used)
     		return send_json_error(api, 400, "bad_request", "Session ID not in use", NULL);
     
    -	// Delete session
    -	const int code = delete_session(uid) ? 204 : 404;
    +	const int code = delete_session(uid, true) ? 204 : 404;
    +	AUTOUNLOCK();
     
     	// Send empty reply with appropriate HTTP status code
     	send_http_code(api, "application/json; charset=utf-8", code, "");
    
  • src/api/config.c+2 2 modified
    @@ -1106,7 +1106,7 @@ int api_config(struct ftl_conn *api)
     
     	// Check if this is an app session and reject the request if app sudo
     	// mode is disabled
    -	if(api->session != NULL && api->session->app && !config.webserver.api.app_sudo.v.b)
    +	if(api->session.used && api->session.app && !config.webserver.api.app_sudo.v.b)
     	{
     		return send_json_error(api, 403,
     		                       "forbidden",
    @@ -1115,7 +1115,7 @@ int api_config(struct ftl_conn *api)
     	}
     
     	// Check if this is a CLI session and reject the request
    -	if(api->session != NULL && api->session->cli)
    +	if(api->session.used && api->session.cli)
     	{
     		return send_json_error(api, 403,
     		                       "forbidden",
    
  • src/api/teleporter.c+2 2 modified
    @@ -221,7 +221,7 @@ static int api_teleporter_POST(struct ftl_conn *api)
     {
     	// Check if this is an app session and reject the request if app sudo
     	// mode is disabled
    -	if(api->session != NULL && api->session->app && !config.webserver.api.app_sudo.v.b)
    +	if(api->session.used && api->session.app && !config.webserver.api.app_sudo.v.b)
     	{
     		return send_json_error(api, 403,
     		                       "forbidden",
    @@ -230,7 +230,7 @@ static int api_teleporter_POST(struct ftl_conn *api)
     	}
     
     	// Check if this is a CLI session and reject the request
    -	if(api->session != NULL && api->session->cli)
    +	if(api->session.used && api->session.cli)
     	{
     		return send_json_error(api, 403,
     		                       "forbidden",
    
  • src/webserver/civetweb/civetweb.h+1 1 modified
    @@ -951,7 +951,7 @@ void FTL_mbed_debug(void *user_param, int level, const char *file,
     
     // Buffer used for additional "Set-Cookie" headers
     #define PIHOLE_HEADERS_MAXLEN 1024
    -extern char pi_hole_extra_headers[PIHOLE_HEADERS_MAXLEN];
    +extern _Thread_local char pi_hole_extra_headers[PIHOLE_HEADERS_MAXLEN];
     /********************************************************************************************/
     
     
    
  • src/webserver/http-common.c+5 1 modified
    @@ -18,7 +18,11 @@
     // HUGE_VAL
     #include <math.h>
     
    -char pi_hole_extra_headers[PIHOLE_HEADERS_MAXLEN] = { 0 };
    +// Thread-local to prevent races between concurrent civetweb worker threads.
    +// Each request is handled entirely within a single thread, so the buffer is
    +// written by the API handler and then read by send_additional_header() in
    +// the same thread.
    +_Thread_local char pi_hole_extra_headers[PIHOLE_HEADERS_MAXLEN] = { 0 };
     
     // Provides a compile-time flag for JSON formatting
     // This should never be needed as all modern browsers
    
  • src/webserver/http-common.h+1 1 modified
    @@ -54,7 +54,7 @@ struct ftl_conn {
     		bool restart :1;
     		const char *restart_reason;
     	} ftl;
    -	struct session *session;
    +	struct session session;
     
     	struct api_options opts;
     };
    

Vulnerability mechanics

Root cause

"A race condition exists in the HTTP session management subsystem due to unsynchronized access to a shared global buffer."

Attack vector

An unauthenticated attacker on the local network sends concurrent requests to public API endpoints. This exploits a race window between the write of an administrator's session ID into a shared buffer and the read of that buffer by another thread handling the attacker's request. The attacker's response then includes the administrator's session ID, granting them administrative access without credentials [ref_id=1].

Affected code

The vulnerability resides in the HTTP session management subsystem, specifically involving the global buffer `pi_hole_extra_headers` declared in `src/webserver/http-common.c` and the unsynchronized access within `check_client_auth()` and `send_api_auth_status()` in `src/api/auth.c` [ref_id=1]. The patch modifies these files and related headers.

What the fix does

The fix introduces two main changes. First, the global buffer `pi_hole_extra_headers` is changed to be `_Thread_local`, ensuring each worker thread has its own private copy, thus eliminating the race condition between threads writing to and reading from the buffer [patch_id=5532871]. Second, a mutex `auth_lock` is added to protect concurrent access to the session array `auth_data`, and session data is copied rather than using a direct pointer, preventing race conditions during session handling [patch_id=5532871].

Preconditions

  • networkAttacker must be on the local network.
  • authNo authentication is required for the attacker.

Generated on Jun 10, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.

References

2

News mentions

0

No linked articles in our index yet.