$ cat cve-2026-44119-apache-htaccess-expr-file-read.mdx
CVE-2026-44119: Reading files as the httpd user through .htaccess expressions
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