Summary
Pi-hole FTL contains a logic flaw in the session validation function check_client_auth() that causes expired sessions to be automatically renewed instead of rejected. When a request is made with an expired session ID, the server detects the expiration, sets an internal flag, but then unconditionally renews the session timestamp and grants access.
The flag is only evaluated in an unreachable code branch. As a result, any session ID that was valid at any point in time grants permanent administrative access as long as it is periodically used.
Details
The function check_client_auth() in FTL/src/api/auth.c is responsible for validating incoming session IDs. The logic flaw is as follows:
// auth.c:235-294
bool expired = false;
AUTOLOCK(&auth_lock);
for(unsigned int i = 0; i < max_sessions; i++)
{
if(auth_data[i].used &&
strcmp(auth_data[i].sid, sid) == 0)
{
// Detects expiration, sets flag but does NOT return
if(auth_data[i].valid_until < now)
expired = true;
// Assigns user_id regardless of expiration
user_id = i;
break;
}
}
if(user_id > API_AUTH_UNAUTHORIZED)
{
// Reached whether session is expired or not
// unconditionally renews the session timestamp
auth_data[user_id].valid_until = now + config.webserver.session.timeout.v.ui;
...
}
else
{
// The 'expired' flag is only evaluated here
// but this branch is UNREACHABLE when the SID matched,
// because user_id was assigned in the loop above
api->message = expired ? "session expired" : "session unknown";
return API_AUTH_UNAUTHORIZED;
}
When the SID matches an expired session, the execution path is:
auth.c:243 - expired = true is set
auth.c:254 - user_id = i is assigned unconditionally
auth.c:258 - user_id > API_AUTH_UNAUTHORIZED evaluates to true
auth.c:264 - valid_until is renewed: now + timeout
auth.c:291 - the expired flag is never checked on this path
The expired flag is assigned but never evaluated in the code path where the SID matches. It is only referenced in the else branch at line 291, which is unreachable whenever user_id was set inside the loop.
PoC
Step 1 - Authenticate and capture the SID:
curl -sk -X POST http://PI_HOLE_IP/api/auth \
-H "Content-Type: application/json" \
-d '{"password":"ADMIN_PASSWORD"}' | python3 -m json.tool
Note the sid value from the response.
Step 2 - Wait for the session to expire (default timeout: 1800 seconds)
Step 3 - Use the expired SID:
curl -sk http://PI_HOLE_IP/api/auth \
-H "sid: EXPIRED_SID" | python3 -m json.tool
Expected response on a vulnerable target:
{
"session": {
"valid": true,
"sid": "EXPIRED_SID",
...
}
}
The server returns valid: true and renews the session. The SID is now active for another full timeout period. This cycle repeats indefinitely on each use.
Impact
Any session ID that was valid at any point in time provides permanent administrative access to Pi-hole as long as it is periodically used. The session timeout mechanism is completely ineffective.
Summary
Pi-hole FTL contains a logic flaw in the session validation function
check_client_auth()that causes expired sessions to be automatically renewed instead of rejected. When a request is made with an expired session ID, the server detects the expiration, sets an internal flag, but then unconditionally renews the session timestamp and grants access.The flag is only evaluated in an unreachable code branch. As a result, any session ID that was valid at any point in time grants permanent administrative access as long as it is periodically used.
Details
The function
check_client_auth()inFTL/src/api/auth.cis responsible for validating incoming session IDs. The logic flaw is as follows:When the SID matches an expired session, the execution path is:
auth.c:243-expired = trueis setauth.c:254-user_id = iis assigned unconditionallyauth.c:258-user_id > API_AUTH_UNAUTHORIZEDevaluates totrueauth.c:264-valid_untilis renewed:now + timeoutauth.c:291- theexpiredflag is never checked on this pathThe
expiredflag is assigned but never evaluated in the code path where the SID matches. It is only referenced in theelsebranch at line 291, which is unreachable wheneveruser_idwas set inside the loop.PoC
Step 1 - Authenticate and capture the SID:
Note the
sidvalue from the response.Step 2 - Wait for the session to expire (default timeout: 1800 seconds)
Step 3 - Use the expired SID:
Expected response on a vulnerable target:
{ "session": { "valid": true, "sid": "EXPIRED_SID", ... } }The server returns
valid: trueand renews the session. The SID is now active for another full timeout period. This cycle repeats indefinitely on each use.Impact
Any session ID that was valid at any point in time provides permanent administrative access to Pi-hole as long as it is periodically used. The session timeout mechanism is completely ineffective.