Score:2

Rewrite Apache Condition not preventing rewrite from getting applied

in flag

I'm trying to only allow incoming requests to /multi if the Content-Type request header starts with multipart/form-data, while requests to other POST, PUT, or DELETE endpoints must always have the application/json Content-Type.

When doing the following in my web roots .htaccess file (no root access):

# Enforce multipart/form-data content type for /multi routes
RewriteCond %{REQUEST_URI} ^/multi [NC]
RewriteCond %{HTTP:Content-Type} !^multipart/form-data; [NC]
RewriteRule ^ - [R=400,L,END]

# Enforce JSON content type for all other POST, PUT, and DELETE endpoints
RewriteCond %{REQUEST_METHOD} ^(POST|PUT|DELETE)$
RewriteCond %{HTTP:Content-Type} !^application/json [NC]
RewriteCond %{REQUEST_URI} !^/multi [NC]
RewriteRule ^ - [R=400,L,END]

... and then sending a POST /multi with Content-Type: multipart/form-data, I get Apache's 400 Bad Request Response. When I remove the second rewrite block, the request gets through. Why is the second rewrite block applied when sending a POST /multi with Content-Type: multipart/form-data, although I have the RewriteCond %{REQUEST_URI} !^/multi [NC] condition in it, which should prevent it from being applied?

When I check the apache error logs while sending the request with the second rewrite block enabled, I have no error log at all. When I check the Apache Dom Log, I simply have that an incoming request to /multi yielded the 400 response. So it indeed seems to be to the fact that the second rewrite rule is applied, which makes no sense to me.

Full .htaccess File

# Prevent files whose name starts with ".ht" from frontend access
<FilesMatch "^\.ht">
    Require all denied
</FilesMatch>

# Set global Rules for document root directory
Options Indexes FollowSymLinks
Require all granted

# Specify rewrite rules
RewriteEngine On

# Authorization Header seems to be stripped by default, hence explicitly enable it
# Doing it via the rewrite rule is not the native way of doing it, and would need an improved rule to avoid that an
# Authorization HTTP header with an empty value is still passed to PHP
# RewriteCond %{HTTP:Authorization} ^(.*)
# RewriteRule .* - [e=HTTP_AUTHORIZATION:%1]
CGIPassAuth On

# Enforce multipart/form-data content type for /multi route
RewriteCond %{REQUEST_URI} ^/multi [NC]
RewriteCond %{HTTP:Content-Type} !^multipart/form-data; [NC]
RewriteRule ^ - [R=400,L,END]

# Enforce JSON content type for all other POST, PUT, and DELETE endpoints (return a 400 Bad Request otherwise)
RewriteCond %{REQUEST_METHOD} ^(POST|PUT|DELETE)$
RewriteCond %{HTTP:Content-Type} !^application/json [NC]
RewriteCond %{REQUEST_URI} !^/multi [NC]
RewriteRule ^ - [R=400,L,END]

# Route all incoming requests through index.php, except if it's for existing files
RewriteBase /
RewriteRule ^index\.php$ - [L]
RewriteCond %{REQUEST_FILENAME} !-f
RewriteRule . /index.php [L]

# Ensure that png images, javascript and css files are all delivered with correct mime type. mod_mime must be enabled.
AddType application/javascript .js
AddType image/png .png
AddType text/css .css

# Add an 'Expires' HTTP header for all accessed js, css and png image files. mod_expires must be enabled.
ExpiresActive On
ExpiresByType image/png "access plus 1 year"
ExpiresByType application/javascript "access plus 1 year"
ExpiresByType text/css "access plus 1 year"

# Also add a Cache-Control HTTP header for the same cache lifetime of 1 year, to cover all browsers. mod_headers needed.
<FilesMatch "\.(js|css|png)$">
  Header set Cache-Control "public, max-age=31536000"
</FilesMatch>

# Define Image Uploads directory, to be able to access img uploads while uploads are kept outside from document root
RewriteRule ^images/(.*)$ /uploads/imgs/$1 [L]

# Ensure that all icons are delivered with the image/jpeg MIME type
RewriteRule ^icons/ - [E=CONTENT_TYPE:image/png]
kz flag
By “web roots” do you mean the document root directory? What other directives do you have in the .htaccess file?
DevelJoe avatar
in flag
Yep I mean the document root. I'll update the question with remaining contents of the `.htaccess` file. The comments always explain why I did what.
Score:3
kz flag
RewriteCond %{REQUEST_FILENAME} !-f
RewriteRule . /index.php [L]

You have a front-controller pattern later in the .htaccess file that rewrites the request to /index.php. And since this rule only has the L flag (as opposed to END) the rewriting engine starts over with the rewritten URL. (In a .htaccess context the rewrite engine loops until it passes through unchanged).

RewriteCond %{REQUEST_URI} !^/multi [NC]

During the second pass by the rewrite engine the URL is now /index.php (ie. not /multi) so the above condition is successful and the second rule is triggered, sending a 400 response. (The REQUEST_URI server variable is updated between passes by the rewrite engine.)

This "looping" by the rewrite engine is an additional complexity (and "feature") when using mod_rewrite in a directory/.htaccess context. It does not happen (by default) when used in a server (or vitualhost) context (unless you explicitly trigger this behaviour with the PT flag).

RewriteCond %{HTTP:Content-Type} !^multipart/form-data; [NC]

Aside: It looks like that request would be blocked by the first rule, since you appear to have an erroneous(?) ; at the end of the regex? (ie. a request with Content-Type: multipart/form-data only would satisfy the above negated condition.)

You need to either:

  • Use the END flag instead on the later rewrite to /index.php (the front-controller), which stops all processing by the rewrite engine. For example:

    :
    RewriteRule . index.php [END]
    

    (You don't need the slash prefix on the substitution string in the case of an internal rewrite. But you have a RewriteBase directive anyway, which is also not necessary here.)

OR

  • Instead of using REQUEST_URI in the second rule (which gets updated by the rewrite engine) to test the requested URL, use THE_REQUEST instead, which contains the first line of the request headers and does not get updated. THE_REQUEST would contain a string of the form POST /multi/foo HTTP/1.1. For example, instead of the REQUEST_URI condition, you would use something like the following instead:

    # Check the URL as requested by the client, not the rewritten URL
    RewriteCond %{THE_REQUEST} !\s/multi
    

OR

  • Make sure the second rule only applies to the initial request from the client and not the written request (by the later rule). For this you can add an additional condition (first) and check against the REDIRECT_STATUS environment variable. This is empty on the initial request and set to 200 (as in "200 OK" HTTP status) after the first successful rewrite. For example:

    # Only apply this rule to direct requests, not rewritten requests
    RewriteCond %{ENV:REDIRECT_STATUS} ^$
    :
    

Other notes:

RewriteRule ^ - [R=400,L,END]

You don't need to specify both L and END. END does the same as L and more. In a .htaccess context L stops the current pass through the rewrite engine but then starts over. Whereas END (new with Apache 2.4) stops the current pass through the rewrite engine and stops any further processing by the rewrite engine.

However, the use of both L and END are unnecessary here. When specifying an R code outside of the 3xx range then processing stops anyway.

RewriteCond %{REQUEST_URI} ^/multi [NC]
RewriteCond %{HTTP:Content-Type} !^multipart/form-data; [NC]
RewriteRule ^ - [R=400,L,END]

You don't need the condition that checks against the REQUEST_URI server variable (in the first rule) as this check can be performed, more efficiently, in the RewriteRule pattern itself. For example:

RewriteCond %{HTTP:Content-Type} !^multipart/form-data [NC]
RewriteRule ^multi - [NC,R=400]

(Assuming the .htaccess file is in the document root, which comments appear to confirm it is.)

NB: This only applies to the first rule, not the second, which uses a negated expression.

# Define Image Uploads directory, to be able to access img uploads while uploads are kept outside from document root
RewriteRule ^images/(.*)$ /uploads/imgs/$1 [L]

It looks like this rule (near the end of the .htaccess file) is in the wrong place? I'm assuming requests of the form /images/<image> don't map directly to physical files so this rule should go before the front-controller pattern, otherwise it's never going to be processed.

(Nothing is "outside the document root" here, despite what the comment says.)

I sit in a Tesla and translated this thread with Ai:

mangohost

Post an answer

Most people don’t grasp that asking a lot of questions unlocks learning and improves interpersonal bonding. In Alison’s studies, for example, though people could accurately recall how many questions had been asked in their conversations, they didn’t intuit the link between questions and liking. Across four studies, in which participants were engaged in conversations themselves or read transcripts of others’ conversations, people tended not to realize that question asking would influence—or had influenced—the level of amity between the conversationalists.