CVE-2026-44119: Reading files as the httpd user through .htaccess expressionsCVE-2026-44119: lendo arquivos como o usuário do httpd através de expressões no .htaccess
A missing restricted-flag let ap_expr file functions run from .htaccess, so a low-trust Apache user could read files with the httpd process's privileges.Uma flag de restrição ausente deixava as funções de arquivo do ap_expr rodarem a partir do .htaccess, permitindo que um usuário Apache de baixa confiança lesse arquivos com os privilégios do processo httpd.
Disclosure: this issue was reported to the Apache HTTP Server security team, fixed in
2.4.68, and is public as of the 2026-06-08 advisory, if you runhttpdyou should upgrade since there is no configuration-only workaround, and I'm credited as one of several independent finders on this CVE.
Summary
CVE-2026-44119 is a moderate privilege issue (CWE-269, Improper Privilege
Management) in Apache HTTP Server up to and including 2.4.67, it lets a
low-trust .htaccess author, think a tenant on a shared host who can drop
.htaccess files but does not control the main server config, read files
with the privileges of the httpd process, not their own.

The root cause is small and precise, Apache's expression engine (ap_expr)
tags a handful of functions and operators as restricted because they touch
the filesystem: the content-reading functions file(), filesize(), and
filemod(), plus the file-test operators (-d -e -f -s -L -h -x).
When an expression is parsed in an untrusted context the parser is supposed to
receive a restricting flag and refuse those primitives, but several directives
that accept expressions forgot to pass any such flag when the expression came
from a .htaccess file, so file(), which reads a file's contents as the
server user, became callable from per-directory config that untrusted users are
allowed to write.
Because more than one directive shared the mistake, the advisory is titled
"escalation of privilege through expressions in .htaccess in multiple modules",
the directives first patched by name are RewriteCond (mod_rewrite),
SetEnvIfExpr (mod_setenvif), and ProxyFCGISetEnvIf (mod_proxy_fcgi), but
as the fix below shows the problem was never limited to those three, it lived in
the shared parse path every expression directive funnels through, and this writeup
uses ProxyFCGISetEnvIf as the worked example.
Affected versions
| Product | Affected | Fixed | CVSS 3.1 |
|---|---|---|---|
| Apache HTTP Server | 2.4.0 – 2.4.67 | 2.4.68 | 5.5 (Medium) |
Official vector (NVD):
CVSS:3.1/AV:L/AC:L/PR:L/UI:N/S:U/C:H/I:N/A:NTwo parts of that vector are worth reading closely, because they explain why a "read any file the server can read" bug scores only 5.5:
AV:L/PR:L: the precondition is being able to author a.htaccessfile that the server will honor, that's a local, already-somewhat-privileged position (a shared-hosting tenant, a CI job, a user with write access to a served directory), not an anonymous internet attacker.C:H/I:N/A:N: the impact is pure confidentiality, you can read, you can't write or crash anything through this path.
Background: ap_expr and the restricted flag
ap_expr is the little expression language sprinkled across Apache config,
<If>, RewriteCond, Require expr, Header ... expr=, SetEnvIfExpr, and
friends, it evaluates in the httpd worker, which means any function it exposes
runs with the worker's uid (commonly www-data or apache).
Apache's config trust model has two tiers:
- Main config (
httpd.conf, vhosts) is written by the administrator and is fully trusted. .htaccessis per-directory config, on shared hosts it is routinely writable by users who are not the administrator, and it is trusted only as far asAllowOverridelets it be.
Some ap_expr primitives are safe to expose to trusted config but dangerous in
untrusted config, so they're marked restricted in the documentation, they fall
into two groups and the distinction turns out to matter for the fix:
- Content-reading functions:
file('/path')returns a file's contents,filesize('/path')its size,filemod('/path')its modification time. - File-test operators:
-f,-d,-e,-s,-L,-h,-x, existence / type / permission tests on a path, these leak metadata, not contents.
The enforcement mechanism is a flag on the parser, when a caller parses with
ap_expr_parse_cmd(..., AP_EXPR_FLAG_RESTRICTED, ...) the parser refuses the
restricted primitives at parse time, mod_include already used this flag for
server-side-includes, where it blocks both groups, but .htaccess, it turned
out, passed no restricting flag at all, so both groups were reachable.
Root cause
Look at how a directive turns its argument string into a compiled expression, the vulnerable pattern parses with no flags regardless of where the directive came from:
static const char *cmd_setenv(cmd_parms *cmd, void *in_dconf,
const char *arg1, const char *arg2,
const char *arg3)
{
/* arg1/arg3 are attacker-controlled when this lives in a .htaccess */
new->cond = ap_expr_parse_cmd(cmd, arg1, 0, &err, NULL); // no restricting flag
new->subst = ap_expr_parse_cmd(cmd, arg3,
AP_EXPR_FLAG_STRING_RESULT, &err, NULL);
...
}The fix keys off a property of cmd_parms: when a directive is processed inside
a .htaccess, its pool is the temp_pool, that single comparison
distinguishes trusted main-config parsing from untrusted per-directory parsing,
but where the comparison lives, and which flag it sets, changed between two
attempts.
The first attempt (trunk r1933349, shipped in 2.4.67) added that comparison
directly to the three named directives, setting AP_EXPR_FLAG_RESTRICTED in the
.htaccess case, and it was both incomplete and too blunt, incomplete because
other expression directives like Require expr, Header ... expr=, and <If>
also parse ap_expr in .htaccess and were left untouched, which is precisely
why 2.4.67 is still listed as affected, and too blunt because
AP_EXPR_FLAG_RESTRICTED also disables the file-test operators, which would break
the ubiquitous RewriteCond %{REQUEST_FILENAME} !-f idiom that a huge share of
the web relies on.
The real fix (trunk r1935016, backported to 2.4.x as r1935017, shipped in
2.4.68) removes those per-directive guards and instead sets a narrower
flag in the one function every directive's ap_expr_parse_cmd() call routes
through:
/** Don't allow functions/vars that expose content from the filesystem. */
#define AP_EXPR_FLAG_RESTRICTED_FILE_FUNC 16info->flags = flags;
info->module_index = module_index;
/* Use restricted-contents ap_expr() parser in htaccess context. */
if (cmd->pool == cmd->temp_pool) {
info->flags |= AP_EXPR_FLAG_RESTRICTED_FILE_FUNC;
}Because ap_expr_parse_cmd() is just a wrapper around ap_expr_parse_cmd_mi(),
that one guard restricts file(), filesize(), and filemod() for every
directive parsed from a .htaccess, the genuine "multiple modules" fix, while
-f, -d, and the other file-test operators keep working, because the new flag
is wired to match only the content-reading functions:
if (((parms->flags & AP_EXPR_FLAG_RESTRICTED) /* mod_include: all */
&& (prov->restricted & RESTRICTED_ALL))
|| ((parms->flags & AP_EXPR_FLAG_RESTRICTED_FILE_FUNC) /* .htaccess: funcs only */
&& (prov->restricted & RESTRICTED_FILE_FUNC))) {
*parms->err = apr_psprintf(parms->ptemp,
"%s%s not available in restricted context", ...);
}Exploitation
The clean threat model is shared hosting with a FastCGI backend:
- The tenant can write
.htaccessin their docroot, and the host hasAllowOverride FileInfo(or more) enabled, otherwise.htaccessis ignored entirely and none of this is reachable. httpdruns aswww-data, the tenant's PHP-FPM pool runs as the tenant's own uid, so there are files readable bywww-datathat the tenant's own code cannot open.
ProxyFCGISetEnvIf sets environment variables that are handed to the FastCGI
process just before the request is proxied, and both its condition and its value
are ap_expr expressions, put a restricted file read in the value and Apache
reads the file for you, as www-data, then hands you the bytes in an env var:
# httpd evaluates file() as www-data and stuffs the contents into LEAK,
# which is then delivered to the FastCGI app the tenant controls.
ProxyFCGISetEnvIf "true" LEAK "%{file:/var/www/other-tenant/config/secrets.env}"<?php
// Runs as the tenant, but LEAK already holds bytes read by www-data.
echo $_SERVER['LEAK'];The privilege boundary crossed is exactly the one in the CVE title: the tenant
never gains code execution or write access, but they get read access to
anything the httpd user can read, other tenants' configs, credentials, keys,
files deliberately kept out of the tenant's own uid, and the other affected
directives give the same primitive by different means, RewriteCond can reflect
file() output into a redirect Location, and SetEnvIfExpr can turn a
comparison like file('/path') =~ /needle/ into a boolean oracle for
byte-at-a-time blind extraction.
The snippet above is an illustrative minimal reproduction built from the documented directive semantics, not a capture from a specific target, only test it against infrastructure you own or are authorized to assess.
How I found it
I've been building an AI-assisted research workflow and pointing it at Apache's
C codebase, for this class of bug the leverage was in surface mapping and
hypothesis generation: enumerate every directive that feeds a string into
ap_expr, then ask a sharper question than "is this exploitable": which of
these parse sites propagate the restricted flag into the untrusted .htaccess
path, and which silently drop it?, which turns a vague audit into a checklist.
The automation is a force multiplier, not an oracle, every candidate has to be reproduced and confirmed by hand before it's worth anyone's time, an LLM will happily narrate a convincing bug that doesn't exist, so a hypothesis isn't real until it's been reduced to a minimal config and observed doing the wrong thing in a controlled environment, this one survived that bar, several others I chased did not, and it's also a collision bug, ten of us are credited as independent finders on the advisory, a good reminder that "found with tooling" and "found first" are different achievements.
Timeline
- 2026-04-26: a first, partial hardening lands on trunk (
r1933349) and is backported the same day, it ships in2.4.67but does not fully close the hole. - 2026-05-05: reported to the Apache HTTP Server security team.
- 2026-06-05: the complete fix lands on trunk (
r1935016) and is backported to2.4.x(r1935017). - 2026-06-08:
2.4.68released, advisory posted to oss-security. - 2026-07-12: this writeup.
References
- oss-security advisory (CVE-2026-44119)
- Apache HTTP Server 2.4 vulnerabilities
- NVD — CVE-2026-44119
- Expressions in Apache HTTP Server (
ap_expr) mod_proxy_fcgi—ProxyFCGISetEnvIf- Fix revisions:
r1935016(trunk) /r1935017(2.4.x), the complete fix released in2.4.68,r1933349was an earlier, partial hardening in2.4.67
Divulgação: este problema foi reportado ao time de segurança do Apache HTTP Server, corrigido na
2.4.68, e é público desde o advisory de 08/06/2026. Se você rodahttpd, atualize — não existe workaround apenas por configuração. Sou creditado como um de vários descobridores independentes desta CVE.
Resumo
A CVE-2026-44119 é um problema moderado de privilégio (CWE-269, Improper
Privilege Management) no Apache HTTP Server até a 2.4.67 inclusive. Ela permite
que um autor de .htaccess de baixa confiança — pense num inquilino de uma
hospedagem compartilhada que pode deixar arquivos .htaccess mas não controla a
configuração principal do servidor — leia arquivos com os privilégios do
processo httpd, e não com os seus próprios.

A causa raiz é pequena e precisa. O motor de expressões do Apache (ap_expr)
marca um punhado de funções e operadores como restritos porque eles tocam o
sistema de arquivos: as funções de leitura de conteúdo file(), filesize() e
filemod(), mais os operadores de teste de arquivo (-d -e -f -s -L -h -x).
Quando uma expressão é interpretada em um contexto não confiável, o parser
deveria receber uma flag de restrição e recusar essas primitivas. Só que várias
diretivas que aceitam expressões esqueceram de passar qualquer flag desse tipo
quando a expressão vinha de um arquivo .htaccess. Assim, file(), que lê o
conteúdo de um arquivo como o usuário do servidor, tornou-se chamável a partir de
configuração por diretório que usuários não confiáveis têm permissão de escrever.
Como mais de uma diretiva compartilhava o erro, o advisory se chama "escalação de
privilégio através de expressões em .htaccess em múltiplos módulos". As
diretivas corrigidas primeiro, por nome, são RewriteCond (mod_rewrite),
SetEnvIfExpr (mod_setenvif) e ProxyFCGISetEnvIf (mod_proxy_fcgi) — mas,
como a correção abaixo mostra, o problema nunca se limitou a essas três: ele vivia
no caminho de parsing compartilhado por onde toda diretiva de expressão passa.
Este writeup usa o ProxyFCGISetEnvIf como exemplo trabalhado.
Versões afetadas
| Produto | Afetadas | Corrigido | CVSS 3.1 |
|---|---|---|---|
| Apache HTTP Server | 2.4.0 – 2.4.67 | 2.4.68 | 5.5 (Média) |
Vetor oficial (NVD):
CVSS:3.1/AV:L/AC:L/PR:L/UI:N/S:U/C:H/I:N/A:NDuas partes desse vetor merecem leitura atenta, porque explicam por que um bug de "ler qualquer arquivo que o servidor pode ler" tira apenas 5.5:
AV:L/PR:L: a pré-condição é conseguir escrever um.htaccessque o servidor vai honrar. Essa é uma posição local e já de algum modo privilegiada (um inquilino de hospedagem compartilhada, um job de CI, um usuário com acesso de escrita a um diretório servido) — não um atacante anônimo da internet.C:H/I:N/A:N: o impacto é puramente de confidencialidade. Você consegue ler; não consegue escrever nem derrubar nada por esse caminho.
Contexto: o ap_expr e a flag de restrição
O ap_expr é a pequena linguagem de expressões espalhada pela configuração do
Apache — <If>, RewriteCond, Require expr, Header ... expr=,
SetEnvIfExpr e companhia. Ela é avaliada dentro do worker do httpd, o que
significa que qualquer função exposta por ela roda com o uid do worker
(comumente www-data ou apache).
O modelo de confiança da configuração do Apache tem dois níveis:
- Configuração principal (
httpd.conf, vhosts) é escrita pelo administrador e é totalmente confiável. .htaccessé configuração por diretório. Em hospedagens compartilhadas, rotineiramente é gravável por usuários que não são o administrador, e é confiável apenas até onde oAllowOverridepermitir.
Algumas primitivas do ap_expr são seguras de expor em configuração confiável mas
perigosas em configuração não confiável, então são marcadas como restritas na
documentação. Elas caem em dois grupos, e a distinção acaba importando para a
correção:
- Funções de leitura de conteúdo:
file('/caminho')devolve o conteúdo de um arquivo,filesize('/caminho')o tamanho,filemod('/caminho')a data de modificação. - Operadores de teste de arquivo:
-f,-d,-e,-s,-L,-h,-x— testes de existência, tipo e permissão sobre um caminho. Eles vazam metadados, não conteúdo.
O mecanismo de aplicação é uma flag no parser: quando um chamador interpreta com
ap_expr_parse_cmd(..., AP_EXPR_FLAG_RESTRICTED, ...), o parser recusa as
primitivas restritas já em tempo de parsing. O mod_include já usava essa flag
para server-side includes, onde ela bloqueia os dois grupos. Mas o
.htaccess, descobriu-se, não passava flag de restrição alguma — então os dois
grupos ficavam alcançáveis.
Causa raiz
Veja como uma diretiva transforma a string do argumento em uma expressão compilada. O padrão vulnerável interpreta sem nenhuma flag, independentemente de onde a diretiva veio:
static const char *cmd_setenv(cmd_parms *cmd, void *in_dconf,
const char *arg1, const char *arg2,
const char *arg3)
{
/* arg1/arg3 são controlados pelo atacante quando isso vive num .htaccess */
new->cond = ap_expr_parse_cmd(cmd, arg1, 0, &err, NULL); // sem flag de restrição
new->subst = ap_expr_parse_cmd(cmd, arg3,
AP_EXPR_FLAG_STRING_RESULT, &err, NULL);
...
}A correção se apoia em uma propriedade do cmd_parms: quando uma diretiva é
processada dentro de um .htaccess, o pool dela é o temp_pool. Essa única
comparação distingue o parsing confiável da configuração principal do parsing não
confiável por diretório. Mas onde essa comparação vive, e qual flag ela
define, mudou entre duas tentativas.
A primeira tentativa (trunk r1933349, entregue na 2.4.67) adicionou essa
comparação diretamente nas três diretivas nomeadas, definindo
AP_EXPR_FLAG_RESTRICTED no caso do .htaccess. Foi ao mesmo tempo incompleta e
grosseira demais. Incompleta porque outras diretivas de expressão, como
Require expr, Header ... expr= e <If>, também interpretam ap_expr em
.htaccess e ficaram intocadas — que é precisamente por que a 2.4.67 ainda
consta como afetada. E grosseira demais porque AP_EXPR_FLAG_RESTRICTED também
desabilita os operadores de teste de arquivo, o que quebraria o onipresente idioma
RewriteCond %{REQUEST_FILENAME} !-f, do qual uma fatia enorme da web depende.
A correção de verdade (trunk r1935016, retroportada para a 2.4.x como
r1935017, entregue na 2.4.68) remove essas guardas por diretiva e, no
lugar, define uma flag mais estreita na única função por onde a chamada de
ap_expr_parse_cmd() de toda diretiva passa:
/** Don't allow functions/vars that expose content from the filesystem. */
#define AP_EXPR_FLAG_RESTRICTED_FILE_FUNC 16info->flags = flags;
info->module_index = module_index;
/* Use restricted-contents ap_expr() parser in htaccess context. */
if (cmd->pool == cmd->temp_pool) {
info->flags |= AP_EXPR_FLAG_RESTRICTED_FILE_FUNC;
}Como o ap_expr_parse_cmd() é apenas um wrapper em volta do
ap_expr_parse_cmd_mi(), essa única guarda restringe file(), filesize() e
filemod() para toda diretiva interpretada a partir de um .htaccess — a
verdadeira correção de "múltiplos módulos". Enquanto isso, -f, -d e os demais
operadores de teste de arquivo continuam funcionando, porque a nova flag foi
ligada para casar apenas com as funções de leitura de conteúdo:
if (((parms->flags & AP_EXPR_FLAG_RESTRICTED) /* mod_include: tudo */
&& (prov->restricted & RESTRICTED_ALL))
|| ((parms->flags & AP_EXPR_FLAG_RESTRICTED_FILE_FUNC) /* .htaccess: só funções */
&& (prov->restricted & RESTRICTED_FILE_FUNC))) {
*parms->err = apr_psprintf(parms->ptemp,
"%s%s not available in restricted context", ...);
}Exploração
O modelo de ameaça limpo é hospedagem compartilhada com um backend FastCGI:
- O inquilino pode escrever
.htaccessno docroot dele, e o host temAllowOverride FileInfo(ou mais) habilitado — caso contrário o.htaccessé ignorado por completo e nada disso é alcançável. - O
httpdroda comowww-data, e o pool PHP-FPM do inquilino roda com o uid do próprio inquilino. Então existem arquivos legíveis pelowww-dataque o código do próprio inquilino não consegue abrir.
O ProxyFCGISetEnvIf define variáveis de ambiente entregues ao processo FastCGI
logo antes de a requisição ser encaminhada, e tanto a condição quanto o valor dele
são expressões ap_expr. Coloque uma leitura de arquivo restrita no valor e o
Apache lê o arquivo para você, como www-data, e depois te entrega os bytes em
uma variável de ambiente:
# o httpd avalia file() como www-data e enfia o conteúdo em LEAK,
# que é então entregue à aplicação FastCGI que o inquilino controla.
ProxyFCGISetEnvIf "true" LEAK "%{file:/var/www/other-tenant/config/secrets.env}"<?php
// Roda como o inquilino, mas LEAK já contém bytes lidos pelo www-data.
echo $_SERVER['LEAK'];A fronteira de privilégio atravessada é exatamente a do título da CVE: o inquilino
nunca ganha execução de código nem acesso de escrita, mas ganha acesso de
leitura a qualquer coisa que o usuário do httpd consiga ler — configurações de
outros inquilinos, credenciais, chaves, arquivos deliberadamente mantidos fora do
uid do próprio inquilino. E as outras diretivas afetadas dão a mesma primitiva por
outros meios: o RewriteCond pode refletir a saída de file() para dentro do
Location de um redirecionamento, e o SetEnvIfExpr pode transformar uma
comparação como file('/caminho') =~ /agulha/ em um oráculo booleano para
extração cega byte a byte.
O trecho acima é uma reprodução mínima ilustrativa, construída a partir da semântica documentada da diretiva, e não uma captura de um alvo específico. Teste apenas contra infraestrutura que você possui ou está autorizado a avaliar.
Como eu encontrei
Venho construindo um fluxo de pesquisa assistido por IA e apontando ele para a
base de código em C do Apache. Para essa classe de bug, a alavancagem estava no
mapeamento de superfície e na geração de hipóteses: enumerar toda diretiva que
alimenta uma string no ap_expr, e então fazer uma pergunta mais afiada do que "isso
é explorável?" — quais desses pontos de parsing propagam a flag de restrição para
o caminho não confiável do .htaccess, e quais a descartam silenciosamente?. Isso
transforma uma auditoria vaga em um checklist.
A automação é um multiplicador de força, não um oráculo. Todo candidato precisa ser reproduzido e confirmado na mão antes de valer o tempo de alguém. Um LLM narra alegremente um bug convincente que não existe, então uma hipótese só é real depois de reduzida a uma configuração mínima e observada fazendo a coisa errada em um ambiente controlado. Esta sobreviveu a essa barra; várias outras que persegui, não. E é também um bug de colisão: dez de nós somos creditados como descobridores independentes no advisory — um bom lembrete de que "encontrado com ferramental" e "encontrado primeiro" são conquistas diferentes.
Linha do tempo
- 26/04/2026: um primeiro endurecimento parcial entra no trunk (
r1933349) e é retroportado no mesmo dia; ele sai na2.4.67, mas não fecha o buraco por completo. - 05/05/2026: reportado ao time de segurança do Apache HTTP Server.
- 05/06/2026: a correção completa entra no trunk (
r1935016) e é retroportada para a2.4.x(r1935017). - 08/06/2026:
2.4.68lançada, advisory publicado na oss-security. - 12/07/2026: este writeup.
Referências
- Advisory na oss-security (CVE-2026-44119)
- Vulnerabilidades do Apache HTTP Server 2.4
- NVD — CVE-2026-44119
- Expressões no Apache HTTP Server (
ap_expr) mod_proxy_fcgi—ProxyFCGISetEnvIf- Revisões da correção:
r1935016(trunk) /r1935017(2.4.x), a correção completa lançada na2.4.68; ar1933349foi um endurecimento parcial anterior, na2.4.67