-----BEGIN PGP SIGNED MESSAGE----- Hash: SHA256 NEFARIOUSPLAN-CANONICAL-V1 {"body_md":"## The patch is twelve lines, in three modules\n\nApache HTTP Server 2.4.67 [shipped on 2026-05-04](https://httpd.apache.org/security/vulnerabilities_24.html) with the fix for CVE-2026-24072, credited to a researcher who reports as `y7syeu` and filed the report with the project on [2026-01-20](https://seclists.org/oss-sec/2026/q2/386). The release closed several issues; the sibling double-free in `mod_http2` is the [one we covered on May 9](https://nefariousplan.com/posts/apache-h2-spurge-was-an-ihash). This is the other one in the same release notes, scored lower because it needs local `.htaccess` write access, and the more telling one in the long run because the fix is not a fix to mod_rewrite.\n\nThe fix is one upstream commit, r1933350, that touches three files. Each file gets the same three-line addition:\n\n```c\nunsigned int flags = 0;\nif (cmd->pool == cmd->temp_pool) {\n flags |= AP_EXPR_FLAG_RESTRICTED;\n}\n```\n\nThe `cmd->pool == cmd->temp_pool` check is the canonical Apache idiom for \"this directive is being parsed inside a `.htaccess` file.\" The per-directory configuration walker calls each directive's `cmd_func` with the two pools aliased to each other when the directive comes from a `.htaccess`; in main-server config and `` contexts they are distinct. The bool is therefore \"this directive's text was written by whoever could put a file in this directory,\" and the flag added on the true branch is \"and they should not be allowed to ask `file()` what is in `/etc/passwd`.\"\n\nThe three call sites that receive the flag post-patch:\n\n`modules/mappers/mod_rewrite.c:3688-3699`, the `RewriteCond expr=...` branch (the `RewriteCond` directive itself is declared `OR_FILEINFO` at `mod_rewrite.c:5732`, so it is parsable in any `.htaccess` whose containing directory permits `AllowOverride FileInfo` or `All`):\n\n```c\nelse if (newcond->ptype == CONDPAT_AP_EXPR) {\n int in_htaccess = cmd->pool == cmd->temp_pool;\n unsigned int flags = newcond->flags & CONDFLAG_NOVARY ?\n AP_EXPR_FLAG_DONT_VARY : 0;\n /* Use restricted ap_expr() parser in htaccess context. */\n if (in_htaccess) flags |= AP_EXPR_FLAG_RESTRICTED;\n newcond->expr = ap_expr_parse_cmd(cmd, a2, flags, &err, NULL);\n if (err)\n return apr_psprintf(cmd->pool, \"RewriteCond: cannot compile \"\n \"expression%s \\\"%s\\\" %s\",\n in_htaccess ? \" in htaccess context\" : \"\",\n a2, err);\n}\n```\n\n`modules/metadata/mod_setenvif.c:438-467`, the `SetEnvIfExpr` directive parser (`OR_FILEINFO`, declared at `mod_setenvif.c:491`):\n\n```c\nunsigned int flags = 0;\n\n/* Use restricted ap_expr() parser in htaccess context. */\nif (cmd->pool == cmd->temp_pool) {\n flags |= AP_EXPR_FLAG_RESTRICTED;\n}\n/* ... */\nnew->expr = ap_expr_parse_cmd(cmd, expr, flags, &err, NULL);\n```\n\n`modules/proxy/mod_proxy_fcgi.c:1345-1353`, the `ProxyFCGISetEnvIf` directive parser (`OR_FILEINFO`, declared at `mod_proxy_fcgi.c:1401`):\n\n```c\nunsigned int flags = 0;\n\n/* Use restricted ap_expr() parser in htaccess context. */\nif (cmd->pool == cmd->temp_pool) {\n flags |= AP_EXPR_FLAG_RESTRICTED;\n}\n\nnew = apr_array_push(dconf->env_fixups);\nnew->cond = ap_expr_parse_cmd(cmd, arg1, flags, &err, NULL);\n```\n\nThat is the patch. Twelve added lines, three modules, one idiom repeated three times. The official cve.org title is \"Apache HTTP Server: mod_rewrite elevation of privileges via ap_expr.\" \"mod_rewrite\" is the headline name. The actual patch is to three modules, and the architecture that produced it is in more.\n\n## The .htaccess that reads /etc/passwd\n\nThe expressions the patched modules now reject are not exotic. They are documented `ap_expr` primitives that the [Apache HTTP Server Expression Parser manual](https://httpd.apache.org/docs/2.4/expr.html) advertises by name. From `server/util_expr_eval.c:2102` (unchanged across 2.4.66 and 2.4.67), the string-function provider table:\n\n```c\nstatic const struct expr_provider_single string_func_providers[] = {\n { osenv_func, \"osenv\", NULL, 0 },\n { env_func, \"env\", NULL, 0 },\n /* ... */\n { file_func, \"file\", NULL, 1 },\n { filesize_func, \"filesize\", NULL, 1 },\n { filemod_func, \"filemod\", NULL, 1 },\n /* ... */\n};\n```\n\nThe fourth column is the `restricted` flag on the provider. It has always been set on the filesystem primitives. The `unary_op_providers` table at `util_expr_eval.c:2131` carries the same column on `-d`, `-e`, `-f`, `-s`, `-L`, `-h`, and `-x`. The `AP_EXPR_FLAG_RESTRICTED` parse flag, when set, checks that column at provider-lookup time and refuses to bind the provider:\n\n```c\nif ((parms->flags & AP_EXPR_FLAG_RESTRICTED)\n && prov->restricted) {\n *parms->err =\n apr_psprintf(parms->ptemp,\n \"%s%s not available in restricted context\",\n (parms->type == AP_EXPR_FUNC_STRING) ? \"\" : \"-\",\n prov->name);\n return !OK;\n}\n```\n\nBoth halves of the mechanism have been in `util_expr_eval.c` for the full fifteen-year life of the expression framework. The 2.4.67 patch does not add the mechanism. It does not change the providers table. It does not change `lookup_provider`. It changes three callers that ask `lookup_provider` to enforce the table.\n\nA pre-patch `.htaccess` written by anyone who can drop a file into a `.htaccess`-honored directory gets the providers without the gate:\n\n```apache\nRewriteEngine On\n\n# Probe: does /etc/shadow exist as a regular file?\nRewriteCond expr \"-f /etc/shadow\"\nRewriteRule .* /found-it [R=302,L]\n\n# Read: the contents of /etc/passwd, base64-encoded into a redirect\nRewriteCond %{REQUEST_URI} ^/leak$\nRewriteRule .* \"expr=%{base64:%{file:'/etc/passwd'}}\" [R=302,L]\n```\n\nThe expressions are bound at parse time, when the worker loads the `.htaccess`. The `file()` function and the `-f` operator run inside the httpd worker process. On Debian and Ubuntu that worker runs as the user `www-data`. On RHEL and derivatives it runs as `apache`. Either user can read `/etc/passwd` because everyone can; it can also read `/var/www//wp-config.php` on a shared host, and `/var/lib/mysql/.MYD` if MySQL keeps its data files group-readable by the web user for socket access. The `.htaccess` author, by design, was supposed to be cabined to URL rewriting inside their own directory. The bug is that the cabinet had no walls.\n\nOn patched 2.4.67 the same `.htaccess` causes the worker to log:\n\n```\nRewriteCond: cannot compile expression in htaccess context \"-f /etc/shadow\" -f not available in restricted context\n```\n\nand reject the file. The same `.htaccess` on a 2.4.66 release reads the file.\n\n## The flag was opt-in, and two modules took it up\n\nThe `AP_EXPR_FLAG_RESTRICTED` machinery is the second-cleanest piece of Apache's expression-parser design, after the providers table itself. The flag tells the parser \"I am parsing an expression supplied by a less-trusted caller than the main config; refuse the file-system providers and any other variable marked confidential.\" The \"less-trusted caller\" the framework was designed around was a specific one: server-side-include directives in `.shtml` files. `mod_include`'s call site has been in the file from the same commit that introduced the framework, at `modules/filters/mod_include.c:1601`:\n\n```c\nexpr_info->flags = AP_EXPR_FLAG_RESTRICTED;\n```\n\nThat is the consumer the comment in `ap_expr.h` was naming. `mod_include` parses `` directives, and the threat model is identical to the `.htaccess` threat model: whoever wrote the `.shtml` may be a different and lower-privileged party than whoever started the httpd. The flag was the fix for that threat in 2011.\n\nA second consumer appeared in 2014, when `mod_authnz_fcgi` was added to the tree. Its directive expressions are read from authentication FastCGI configuration, and the author was security-aware. From `modules/aaa/mod_authnz_fcgi.c:1142`:\n\n```c\nint flags = AP_EXPR_FLAG_DONT_VARY | AP_EXPR_FLAG_RESTRICTED;\n```\n\nThose were the two modules that, before 2.4.67, set the flag. The rest of the tree called `ap_expr_parse_cmd` without it. The grep, against May 4 trunk, with `AP_EXPR_FLAG_RESTRICTED` as the pattern, matched against module files only:\n\n```\nmodules/filters/mod_include.c:1601: expr_info->flags = AP_EXPR_FLAG_RESTRICTED;\nmodules/aaa/mod_authnz_fcgi.c:1142: int flags = AP_EXPR_FLAG_DONT_VARY | AP_EXPR_FLAG_RESTRICTED;\nmodules/mappers/mod_rewrite.c:3693: if (in_htaccess) flags |= AP_EXPR_FLAG_RESTRICTED; /* added in 2.4.67 */\nmodules/metadata/mod_setenvif.c:442: flags |= AP_EXPR_FLAG_RESTRICTED; /* added in 2.4.67 */\nmodules/proxy/mod_proxy_fcgi.c:1349: flags |= AP_EXPR_FLAG_RESTRICTED; /* added in 2.4.67 */\n```\n\nFive consumers. Two of them existed before 2026-01-20, the date `y7syeu` filed the report. The other three are the patch.\n\n## Four other .htaccess-allowed directives parse ap_expr without the flag\n\nThe advisory's \"various modules\" is, on the trunk source the patch was committed against, four more modules than the patch covers. Each of the following is a directive parser that accepts an `ap_expr` expression (commonly as the `expr=...` syntax), has an override class that makes it allowable in `.htaccess` (`OR_FILEINFO` or `OR_OPTIONS`), and calls `ap_expr_parse_cmd` with `AP_EXPR_FLAG_RESTRICTED` absent from its flags.\n\n**`mod_headers.c`** declares `Header` and `RequestHeader` at `modules/metadata/mod_headers.c:1047` and `:1050` with `OR_FILEINFO`. The `expr=...` clauses are parsed at two sites:\n\n- `mod_headers.c:398`, the value branch (`Header set X-Foo \"expr=...\"`):\n\n ```c\n hdr->expr_out = ap_expr_parse_cmd(cmd, s+5,\n AP_EXPR_FLAG_STRING_RESULT,\n &err, NULL);\n ```\n\n- `mod_headers.c:533`, the conditional `envclause` branch (`Header set X-Foo value \"expr=...\"`):\n\n ```c\n expr = ap_expr_parse_cmd(cmd, envclause + 5, 0, &err, NULL);\n ```\n\nNeither call passes `AP_EXPR_FLAG_RESTRICTED`. A `.htaccess` of `Header set X-Leak \"expr=%{file:'/etc/passwd'}\"` parses without complaint on 2.4.67 and writes the file contents into the response header. The CVE description and the 2.4.67 release notes do not mention `mod_headers`.\n\n**`mod_alias.c`** declares `Redirect` at `modules/mappers/mod_alias.c:754` with `OR_FILEINFO`, and `RedirectMatch`, `RedirectTemp`, `RedirectPermanent` at adjacent lines with the same override. The redirect URL is parsed at `mod_alias.c:313`:\n\n```c\ndirconf->redirect =\n ap_expr_parse_cmd(cmd, url, AP_EXPR_FLAG_STRING_RESULT,\n &expr_err, NULL);\n```\n\n`AP_EXPR_FLAG_STRING_RESULT` without `AP_EXPR_FLAG_RESTRICTED`. A `.htaccess` of `Redirect / \"expr=%{base64:%{file:'/etc/passwd'}}\"` exfiltrates via the `Location:` header on the first request to that directory. The CVE description does not mention `mod_alias`.\n\n**`mod_substitute.c`** declares `Substitute` at `modules/filters/mod_substitute.c:809` with `OR_FILEINFO`. The expression replacement is parsed at `mod_substitute.c:749`:\n\n```c\nnscript->expr_replacement = ap_expr_parse_cmd(cmd, to+5,\n AP_EXPR_FLAG_STRING_RESULT,\n &err, NULL);\n```\n\n`Substitute \"s/PLACEHOLDER/expr=%{file:'/etc/passwd'}/\"` injects file contents into outgoing response bodies that match. The CVE description does not mention `mod_substitute`.\n\n**`mod_filter.c`** declares `FilterProvider` at `modules/filters/mod_filter.c:744` with `OR_OPTIONS`. The expression is parsed at `mod_filter.c:472`:\n\n```c\nnode = ap_expr_parse_cmd(cmd, expr, 0, &err, NULL);\n```\n\nFlags are zero. The expression is evaluated at request time, in the worker, as the httpd user. The CVE description does not mention `mod_filter`.\n\nThe patch fixes three modules. The grep for `ap_expr_parse_cmd` against `.htaccess`-allowed directives on the post-patch tree finds four more sites the patch did not touch, across four other modules, all of which evaluate their expressions in the same worker process as the same httpd user with the same access to the same filesystem.\n\n## The check is on the consumer side and the producers were trusted to ask\n\nThe reason for the gap is in the shape of the framework, not in any single module's source.\n\n`ap_expr` is a parse-once-evaluate-many design. A directive handler calls `ap_expr_parse_cmd` with the expression text and a flags word that governs what the parser is willing to bind. The framework parses the expression to a tree, the handler stashes the tree, and at request time the framework evaluates it. The `restricted` check is at `lookup_provider`, which runs during parsing: if the caller asked for restriction and the provider is marked restricted, the lookup fails and the parse fails with the directive's containing `.htaccess` rejected.\n\nThe shape of that contract is \"the producer of the expression text decides the trust level, and the framework enforces it.\" It is the opposite of deny-by-default. The framework's safe behavior is to allow `file()` and `-f` unless the directive handler specifically remembers that those primitives should be denied for this particular caller's trust context. The doc-comment names one consumer that remembered. It does not name what trust contexts the framework considers high-risk. It does not enumerate which override classes (`OR_FILEINFO`, `OR_OPTIONS`, `OR_INDEXES`, `OR_LIMIT`) make a directive `.htaccess`-reachable and therefore in need of the flag. The contract was \"if you ship a directive that takes untrusted expression text, you know to set the flag.\" `mod_include`'s author knew. `mod_authnz_fcgi`'s author knew. The authors of mod_rewrite, mod_setenvif, mod_proxy_fcgi, mod_headers, mod_alias, mod_substitute, and mod_filter wrote their `ap_expr_parse_cmd` lines independently, in different files, against different reviews, and inherited none of the gate.\n\nThis is the [parallel-implementation-gap](https://nefariousplan.com/patterns/parallel-implementation-gap) pattern in module form. The check that gates `file()` is at the bottom of the parser, inside `lookup_provider`. The capability to ask for that gate is on a flag the directive handler must pass. The framework's documentation names one consumer that it was designed around. Every other consumer in the tree was written separately, by a different author, for a different directive, in a different file, and either knew about the flag or did not. The 2.4.67 patch makes three of those consumers look like the canonical consumer. The compiler does not know how many it missed.\n\nThe vendor's advisory says \"various modules.\" The cve.org title names mod_rewrite. The patch fixes three modules. The `ap_expr_parse_cmd` grep against the `.htaccess`-allowed directive set on trunk finds four more.\n\nThe fix is not a new mechanism. The mechanism has existed for fifteen years. The fix is three lines that say *please use it*, in three of the modules that never did.\n\nPoC: [EricRHancock-coder/CVE-2026-24072-Analysis](https://github.com/EricRHancock-coder/CVE-2026-24072-Analysis). Patch: r1933350, [Apache HTTP Server 2.4.67 security advisories](https://httpd.apache.org/security/vulnerabilities_24.html). Disclosure: [oss-security thread, 2026-05-04](https://seclists.org/oss-sec/2026/q2/386).","closing_line":"The comment named one consumer. The CVE names three more. The grep on trunk this morning names four that the CVE still does not.","hook_md":"`AP_EXPR_FLAG_RESTRICTED` is defined at `include/ap_expr.h:66`. It has been defined there since the commit that introduced Apache's expression-parser framework in mid-2011. The comment that declares it names exactly one consumer:\n\n```c\n/** Don't allow functions/vars that bypass the current request's access\n * restrictions or would otherwise leak confidential information.\n * Used by e.g. mod_include.\n */\n#define AP_EXPR_FLAG_RESTRICTED 4\n```\n\nThe grep for the flag's name across the trunk source tree, on the morning of 2026-05-04 (the day the Apache Software Foundation disclosed CVE-2026-24072), returned five matches. One was the definition above. Two were the line that reads the flag during expression evaluation. The other two were the only modules that ever set it: `mod_include` and `mod_authnz_fcgi`. No other directive parser in the tree asked the parser to refuse `file('/etc/passwd')` when the directive came from a `.htaccess`.\n\nCVE-2026-24072 is the bill for fifteen years of `e.g.`","post_id":273,"slug":"apache-ap-expr-restricted-was-opt-in","title":"CVE-2026-24072: AP_EXPR_FLAG_RESTRICTED Was Opt-In For Fifteen Years. The Patch Opts In Three Modules. Four More Still Read /etc/passwd From .htaccess.","type":"initial","unreadable_sentence":"The fix is not a new mechanism. The mechanism has existed for fifteen years. The fix is three lines that say *please use it*, in three of the modules that never did."} -----BEGIN PGP SIGNATURE----- iHUEARYIAB0WIQRf0htP5+SjynlxywneZjl4jgkQJgUCasUq7AAKCRDeZjl4jgkQ JqRnAQC78c3eh32muvBfy+4T7ei+d4iq/BHjgOAdGfbGGLYtJwEAk9CG+a0Imm+Y cjsSq4iF9dEJDQ/W7TXKdBCCgyXm4wg= =37Po -----END PGP SIGNATURE-----