joaovicdev
postartigoCVE-2026-44119

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 run httpd you 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.

NVD detail page for CVE-2026-44119, showing the description, the 5.5 MEDIUM CVSS 3.1 vector, and the advisory credit list with joaovicdev among the finders

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

ProductAffectedFixedCVSS 3.1
Apache HTTP Server2.4.0 – 2.4.672.4.685.5 (Medium)

Official vector (NVD):

CVSS:3.1/AV:L/AC:L/PR:L/UI:N/S:U/C:H/I:N/A:N

Two 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 .htaccess file 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.
  • .htaccess is per-directory config, on shared hosts it is routinely writable by users who are not the administrator, and it is trusted only as far as AllowOverride lets 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:

modules/proxy/mod_proxy_fcgi.c (vulnerable, pre-2.4.67)
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:

include/ap_expr.h (fixed): a new, narrower flag
/** Don't allow functions/vars that expose content from the filesystem. */
#define AP_EXPR_FLAG_RESTRICTED_FILE_FUNC   16
server/util_expr_eval.c, ap_expr_parse_cmd_mi() (fixed, central)
info->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:

server/util_expr_eval.c, core_expr_lookup() (fixed)
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 .htaccess in their docroot, and the host has AllowOverride FileInfo (or more) enabled, otherwise .htaccess is ignored entirely and none of this is reachable.
  • httpd runs as www-data, the tenant's PHP-FPM pool runs as the tenant's own uid, so there are files readable by www-data that 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:

.htaccess (illustrative minimal PoC)
# 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}"
index.php (attacker-controlled FastCGI app)
<?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

  1. 2026-04-26: a first, partial hardening lands on trunk (r1933349) and is backported the same day, it ships in 2.4.67 but does not fully close the hole.
  2. 2026-05-05: reported to the Apache HTTP Server security team.
  3. 2026-06-05: the complete fix lands on trunk (r1935016) and is backported to 2.4.x (r1935017).
  4. 2026-06-08: 2.4.68 released, advisory posted to oss-security.
  5. 2026-07-12: this writeup.

References

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ê roda httpd, 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.

Página de detalhe da NVD para a CVE-2026-44119, mostrando a descrição, o vetor CVSS 3.1 de 5.5 MÉDIO, e a lista de créditos do advisory com joaovicdev entre os descobridores

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

ProdutoAfetadasCorrigidoCVSS 3.1
Apache HTTP Server2.4.0 – 2.4.672.4.685.5 (Média)

Vetor oficial (NVD):

CVSS:3.1/AV:L/AC:L/PR:L/UI:N/S:U/C:H/I:N/A:N

Duas 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 .htaccess que 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 o AllowOverride permitir.

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:

modules/proxy/mod_proxy_fcgi.c (vulnerável, pré-2.4.67)
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:

include/ap_expr.h (corrigido): uma flag nova e mais estreita
/** Don't allow functions/vars that expose content from the filesystem. */
#define AP_EXPR_FLAG_RESTRICTED_FILE_FUNC   16
server/util_expr_eval.c, ap_expr_parse_cmd_mi() (corrigido, central)
info->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:

server/util_expr_eval.c, core_expr_lookup() (corrigido)
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 .htaccess no docroot dele, e o host tem AllowOverride FileInfo (ou mais) habilitado — caso contrário o .htaccess é ignorado por completo e nada disso é alcançável.
  • O httpd roda como www-data, e o pool PHP-FPM do inquilino roda com o uid do próprio inquilino. Então existem arquivos legíveis pelo www-data que 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:

.htaccess (PoC mínima ilustrativa)
# 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}"
index.php (aplicação FastCGI controlada pelo atacante)
<?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

  1. 26/04/2026: um primeiro endurecimento parcial entra no trunk (r1933349) e é retroportado no mesmo dia; ele sai na 2.4.67, mas não fecha o buraco por completo.
  2. 05/05/2026: reportado ao time de segurança do Apache HTTP Server.
  3. 05/06/2026: a correção completa entra no trunk (r1935016) e é retroportada para a 2.4.x (r1935017).
  4. 08/06/2026: 2.4.68 lançada, advisory publicado na oss-security.
  5. 12/07/2026: este writeup.

Referências